Skip to content

[ Feature - Dashboard ] Topology UI/UX #128

Description

@mitulgarg

Phase 3 — Topology grouping UX

 This is the heaviest phase. Three concerns: filter (top bar), assignment (canvas multi-select), rendering (cluster +
 bubbles).

 3a. Top toolbar — web/src/pages/TopologyView.tsx

 Add a thin toolbar above the canvas:

 [ Group: All ▾ ]   [ + New group ]   [ 🔍 search hostname ]

 - Group: [All ▾] — dropdown sourced from getGroups(). Selecting a group filters the canvas: nodes outside the group
 are hidden (filtered out of the simulation entirely so they don't tug from off-screen). Default = "All". Special
 entries: All (no filter), Ungrouped (group_name IS NULL).
 - + New group — opens a tiny inline input ("Group name…" + Enter to confirm, Esc to cancel). Creates an empty group
 locally — it appears in the dropdown; it persists once a machine is assigned to it (see "implicit creation" in
 Phase 2).
 - 🔍 search — fuzzy substring match on hostname. Matched nodes pulse / non-matches dim.

 This is the primary "too many GPUs" mitigation: scope the canvas, don't try to render 80 nodes at once.

 3b. Canvas multi-select — TopologyView.tsx

 Extend the existing mouse handlers (onMouseDown ~line 544, onMouseMove ~line 568):

 - Shift+click on node → toggle membership in selectedIds: Set<string>. Selected nodes get a thicker ring (extend
 drawGpuNode).
 - Drag on empty canvas with no modifier currently pans. Change: drag on empty canvas with Shift held = lasso. Render
 a translucent rect; on mouse up, intersect with node positions → bulk-add to selectedIds. Plain drag-on-empty still
 pans (preserves current muscle memory).
 - Esc / click empty (no shift) → clear selection.
 - Disambiguation rule: if mouseDown hits a node → drag = move that node (current behaviour). If mouseDown is empty +
 shift → lasso. If mouseDown is empty + no shift → pan. Single-node drag is unchanged so existing users notice
 nothing.

 3c. Floating action bar — new web/src/components/SelectionActionBar.tsx

 Renders absolutely positioned at bottom-center of the topology viewport when selectedIds.size > 0:

 [ 3 selected ]  [ Assign group ▾ ]  [ Ungroup ]  [ × ]

 - "Assign group ▾" → opens <GroupPicker> (the same component used in MachineDetail). On select → fire
 updateMachineGroup() for each selected id in parallel (Promise.all); on completion clear selection + refetch.
 - "Ungroup" → updateMachineGroup(id, null) for each.
 - "×" → clear selection.
 - Show error toast if any PATCH fails (preserve selection so user can retry).

 3d. Right-click shortcut

 Single-node oncontextmenu (the canvas already swallows mouse events at ~line 540) → render a tiny <GroupPicker>
 popover at the cursor. Keyboard-friendly alternative for the one-off case. No selection model needed for this path —
 it operates on the right-clicked node directly.

 3e. Group rendering inside the canvas

 Extend GNode interface (~line 24-38) to include group: string | null. Update the sim:

 - Same-group attraction: add a soft pull between nodes sharing a group value. Cheap O(n²) pass: for each pair with
 a.group === b.group && a.group !== null, apply attractive force k_group * (dist - GROUP_IDEAL) with GROUP_IDEAL = 110
  (half the hub distance), k_group = 0.0008. Tune.
 - Group bubbles: before drawing nodes, compute centroid + bounding radius for each group → draw a faint filled circle
  (alpha ~0.08, same colour as the group's hash → consistent palette). Label the group name above the bubble. Skip
 bubble drawing if the dropdown is filtered to a single group (redundant).
 - Hash group name → HSL hue (hashCode(name) % 360, 60% sat, 50% light, 8% alpha) for stable per-group colours without
  a colour picker.

 3f. Refetch wiring

 After any PATCH from the action bar / right-click / picker, call the existing getMachines() poller's load function
 (extract it into a useCallback so it can be invoked imperatively).

 ---
 Phase 3.5 — Fleet integration

 Small additions, share work with Phase 3 components:

 - web/src/pages/FleetOverview.tsx: add Group column between Hostname and GPU. Add a group filter chip next to the
 existing status pills, sourced from getGroups(). Clicking a group name in the column also filters.
 - The filter chip uses the same <GroupPicker> in read/select-only mode (passing onChange to update local filter state
  instead of calling the API).

 ---
 Phase 4 — Sidebar reshape + landing redirect

 Final UX polish, ship last so each preceding phase can validate independently.

 - web/src/App.tsx:108-115 reorder nav: Fleet → Topology → Activity (Fleet first since it's the workhorse).
 - web/src/main.tsx:13-17 change index route: <Route index element={<Navigate to="/fleet" replace />} /> (was
 /topology).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions