Skip to content

Dashboard design rules

How the dashboard behaves and looks. These rules are binding for any change to the dashboard UI: a page that is added or redesigned follows all of them. The Operations page and the Operations record settings follow them; the RAN page follows them with the exception listed at the end. Older pages still use the previous loader (Loader.jsx) and modals for some actions, and move to these rules when they are redesigned.

The frontend's structure (routes, navigation, runtime configuration) is in contributing.md. The building blocks named below live in dashboard/frontend/src/.

1. Every change to the testbed is a piece

A button that changes the testbed starts a piece: a named, idempotent part of a phase playbook, listed in ansible/pieces.yml and run by ansible/tools/kelt-piece (see contributing.md). The CLI runs the same pieces with kelt run-piece, so an action behaves the same from both sides, and every run leaves a record.

Use PieceButton (components/PieceButton.jsx). It takes the title, the tier and the confirmation text from the registry; the page follows the run.

Example: the RAN page's "Bring the RAN link up" fix starts the ran_link piece.

2. Three tiers by effect, three colours

Tier What it does Colour
READ Reads, opens, copies; changes nothing grey (slate)
CHANGE Brings the testbed to the state it should already be in indigo
DISRUPT Interrupts traffic or removes something amber

A DISRUPT action is never grey. The tier of a piece is its tier in pieces.yml.

ConfirmAction colours its button and its confirmation from the tier; a quiet (secondary) button keeps a grey background but takes the tier's text colour.

Example: on the RAN page, "Open the gNB console" is READ, "Bring the RAN link up" (the ran_link piece) is CHANGE, "Detach" is DISRUPT.

3. The confirmation opens inside the card

A CHANGE or DISRUPT action asks for confirmation in place: the card grows a panel that says what the action runs, what it changes and how long it takes; a CHANGE is then one click. A DISRUPT also says what stops, and its confirm button repeats the effect. A piece that cuts devices off (confirm_word in pieces.yml, e.g. Detach) also asks for that word to be typed before the confirm button works. No modal for actions.

Use ConfirmAction (components/Card.jsx). The panel is a t-acc accordion (index.css); its selectors are child combinators, so an accordion inside another opens only itself.

4. A DISRUPT action has its own place

It sits at the foot of its card, under a divider, apart from the routine controls. Its label names the effect ("Detach", not "Apply"). It is shown only while it applies: nothing to detach, no Detach button.

Example: Detach at the foot of the RAN page's path card.

5. Operations: the header and the Operations page

The header's Operations button shows how many pieces are running, with the loader while one is. Its panel lists at most three running operations and the last result, and never scrolls; the rest is on the Operations page (Platform → Operations), which lists the 50 newest runs from the dashboard and the CLI with their steps and read-back; the Ansible output opens on request, or by itself when the run failed. A button whose piece is running opens its operation, or the Operations page when the run was started elsewhere.

Both are admin-only, like the operations API. The record's retention is in Settings → Operations record.

6. One loader, only for work someone waits on

The loader is SignalBars (components/SignalBars.jsx): four bars, 16 px, in the card header's slot (Card's busy). It shows while someone waits for a result: the first read of a card, a run they started. A page that polls shows no loader for its polls and has no manual refresh button; it says how fresh its data is with "updated N s ago" (UpdatedAgo).

7. Numbers resolve in place

A number that arrives or changes resolves digit by digit where it stands, with no layout shift: Num (components/Num.jsx). With reduced motion the value is shown directly.

8. Data comes from useResource

data/useResource.js reads one shared cache per key (lib/resourceCache.js): what was read before is shown at once and refreshed in the background (stale-while-revalidate). Polling pauses while the tab is hidden. A refresh never rebuilds content that is already on screen. A failed refresh keeps the last data and says it failed (Card's error line).

9. Cards keep their own height

A grid of cards uses items-start, so opening a drawer or a confirmation in one card does not stretch its neighbours.

10. House classes, no new UI dependencies

Section labels, cards and buttons use the existing classes and components: btn and inputCls from components/ui.jsx, Card, the node and NF cards (NodeCard, NfCard). A new UI library is not added for a page.

11. "Done" means the state was read back

A run that exits 0 is not yet done: the piece's check reads the state back, and only then does the page say done. Until the read-back has run (it runs when the page follows the run or the run is opened), the run reads "exited 0, not read back yet". A failed read-back is a failure, shown as one. A run opened more than 10 minutes after it ended is marked "not read back" rather than judged by today's state.

12. Values come from the backend

Addresses, ports, subnets and names shown on a page come from the backend, which reads them from ansible/group_vars/all.yml or from the running pods. The UI has no literals for them.

Example: the gNB settings on the RAN page (AMF address and NGAP port, RAN gateway, user-plane route) are read from the backend; the NGAP port is read by name from the running AMF pod.

13. One gate before the shell

The shell (sidebar, header, pages) opens only once the backend and Keycloak have answered and there is a session. Every state before that is one frame, AuthGate (components/AuthGate.jsx), chosen by gateView (lib/authFlow.js): waiting for the services (each listed with its state), opening the sign-in page, completing it, session ended, signed out, sign-in failed, HTTPS needed, access session expired. The auth routes (/auth/callback, /logged-out) are gate states, never pages inside the shell. Keycloak is probed only until the shell opens.

A backend lost with the shell open does not unmount it: the same frame covers the shell ("Backend not reachable"), with the service state and its last messages and, for admins, a restart through the watchdog; it goes away by itself when the backend answers. Meanwhile reads wait and are sent on recovery, so the pages keep their data and their state; writes fail at once (lib/backendState.js, api.js). The gate is the only place with the spinner; inside the shell the loader is SignalBars (6).

14. Toasts under the header

Short results any page raises (useToast, context/ToastContext.jsx) appear at the top right, under the header, never over its controls. A state that holds (an update available, an operation running) is in the header, where its click leads to the place to act on it; it is not repeated as a toast.

Where the dashboard does not follow these yet

  • RAN page, the gNB console address: "Publish" and "Remove" call the API directly; "Publish" has no confirmation panel.
  • Pages not yet redesigned: the previous loader (Loader.jsx), modals for some actions, manual refresh buttons.