From 1e6156d1163e35662d8862d874bf72dd11988398 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 11 Sep 2026 21:44:11 +0200 Subject: [PATCH] swarm-ui/agents: bring back the table view (and its filters) as a toggle Mara's follow-up on #4257, right after the card-only version went up: "still want the filters tho, maybe split the data component from the view" / "if we split data component and view, we can make it switchable between table and specialized card view." AgentsPage already had its data/actions (rows, declareState, the dialog-target state) separated from rendering by this point - the only missing piece was a second renderer. Restores the original Table + columns array as the "table" viewMode alongside the new card view, adds a small two-button toggle in the Panel's actions row, and persists the choice to localStorage (same pattern as Table's own per-column filter persistence) so a reload keeps the last choice. Cards stays the default. Verified with a real CDP click switching to table view: columns, sort arrows, and filter icons are all back. --- .../swarm-ui/src/pages/AgentsPage.css | 34 ++- .../swarm-ui/src/pages/AgentsPage.tsx | 242 ++++++++++++++++-- 2 files changed, 247 insertions(+), 29 deletions(-) diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.css b/frontend/packages/swarm-ui/src/pages/AgentsPage.css index 38e7f78f..d54367ff 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.css +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.css @@ -8,11 +8,43 @@ flex-direction: column; gap: 0.5em; } -.ui-agent-card-list-empty { +/* Shared by both views (`Table` has its own `emptyMessage` rendering, + but the card view has nothing equivalent — this covers both, and + `AgentsPage` only mounts it when `rows` is loaded and empty). */ +.ui-agents-empty { color: var(--muted); text-align: center; padding: 1.25em 0.75em; } +/* Two-way view switcher in the Panel's title-row actions — same visual + weight as `RefreshIntervalPicker` next to it (quiet until touched), + but a segmented pair rather than a select since there's no listbox + worth opening for a two-option choice. */ +.ui-agents-view-toggle { + display: inline-flex; + border: 1px solid var(--border); + border-radius: 0.4em; + overflow: hidden; +} +.ui-agents-view-toggle button { + border: none; + background: none; + color: var(--muted); + padding: 0.45em 0.75em; + font: inherit; + font-size: 0.9em; + cursor: pointer; +} +.ui-agents-view-toggle button + button { + border-left: 1px solid var(--border); +} +.ui-agents-view-toggle button.active { + background: var(--bg-elev); + color: var(--fg); +} +.ui-agents-view-toggle button:not(.active):hover { + color: var(--fg); +} .ui-agent-card { display: flex; flex-direction: column; diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index 24309aa3..b4c1d192 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -1,30 +1,30 @@ // — the swarm's agent roster, merged with each agent's open -// config-PR status, swarm-wide health status, and declared wanted state -// in the same table. Three separate asks (a roster listing, a per-agent -// config-PR indicator, and per-agent status/liveness) that turned out to -// be one page: a roster with no per-row detail is thin, and neither a -// config-PR panel nor a status panel has anything to render against -// without a roster to embed in. +// config-PR status, swarm-wide health status, and declared wanted state. +// A roster with no per-row detail is thin, and neither a config-PR nor a +// status panel has anything to render against without a roster to embed +// in — so all three ended up as one page. // // One fetch, on a refresh-interval cadence like HivesPage: `GET // /api/agents/status` — one row per roster agent (identity store is the // roster, so a never-reported agent still gets a row), each already -// carrying its freshness/snapshot/config PR/wanted state. That merge -// used to be three separate fetches (`/api/agents`, `/api/config-prs`, -// `/api/agents/status`) joined client-side by name; per operator review -// feedback ("the view should be filled by a single backend call") the -// join moved server-side instead — see `swarm-controller/src/main.rs`'s -// `get_agents_status` handler for where `config_pr`/`wanted` get merged -// in and why that's the handler's job rather than -// `agent_status::AgentStatusReader`'s. No more per-page joining left to -// do here: the wire row *is* the row this page renders. +// carrying its freshness/snapshot/config PR/wanted state. Used to be +// three separate fetches joined client-side by name; per operator +// review feedback ("the view should be filled by a single backend +// call") the join moved server-side instead — see +// `swarm-controller/src/main.rs`'s `get_agents_status` handler. No more +// per-page joining left to do here: the wire row *is* the row this page renders. // // Owns the "+ agent" trigger too — the roster this populates is the // natural home for the action that populates it. The form itself // (`CreateAgentForm`) mounts inside a `Dialog` here, not its own route. // -// Renders as one `AgentCard` per row, not a `Table` — see that -// component's own comment below for the card/detail-panel split. +// Two interchangeable views over the same `rows`/actions, picked by +// `viewMode`: the original `Table` (full columns, per-column sort/ +// filter) and one `AgentCard` per row (see that component's own +// comment for the card/detail-panel split). Mara, after the card view +// landed: "still want the filters tho" / "split data component from +// the view... switchable between table and specialized card view" — +// so the toggle, not a card-only replacement. // // The "wanted" column is one `WantedMenu` badge+dropdown per row — see // that component's own comment above `AgentsPage` for why. @@ -45,6 +45,7 @@ import { useRefreshInterval, type RefreshIntervalMs, } from "../ui/refresh-interval/RefreshInterval.js"; +import { Table, type TableColumn } from "../ui/table/Table.js"; import { CreateAgentForm } from "./CreateAgentForm.js"; import { LinkMatrixAccountForm } from "./LinkMatrixAccountForm.js"; import "./AgentsPage.css"; @@ -142,6 +143,11 @@ const CONFIRM_COPY: Record< // solve staleness rather than require an opt-in every visit. const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; +type ViewMode = "cards" | "table"; +// Persisted like `Table`'s own per-column filters (`storageKey` below) — +// a chosen view is a standing preference, not a per-visit default. +const VIEW_MODE_KEY = "swarm-ui:agents:view-mode"; + // The "wanted" column's control: one badge showing the current // declaration, opening a `Dropdown` with the four explicit states — // replaces the old toggle-badge-plus-separate-destroy-badge pair. @@ -247,15 +253,15 @@ function WantedMenu({ } // One card per roster agent (mara: "main view: name, status, message, -// wanted" / "message as second line" / "more like card per agent", -// replacing the old table row). Everything else the old table's other -// columns carried (hive, matrix link-account, config-PR link, destroy) -// moved to the detail panel `AgentsPage` opens on card click — a plain -// `Dialog`, not the shared `hive-side-panel` slide-in drawer: that's a -// shadow-DOM custom element, and swarm-ui's esbuild config can't consume -// those at all yet (same already-tracked gap `Dialog.tsx`'s own comment -// notes for `hive-dialog`). Revisit once that gap closes. The old -// per-column sort/filter has no replacement yet in this view. +// wanted" / "message as second line" / "more like card per agent") — +// the `viewMode === "cards"` alternative to the original `Table`, not +// its replacement (see `AgentsPage`'s own comment). Everything the +// table's other columns carried (hive, matrix link-account, config-PR +// link, destroy) moves to a detail panel on card click instead — a +// plain `Dialog`, not the shared `hive-side-panel` slide-in drawer: +// that's a shadow-DOM custom element, and swarm-ui's esbuild config +// can't consume those yet (same gap `Dialog.tsx`'s own comment already +// flags for `hive-dialog`). Revisit once that gap closes. // // A `role="button"` div, not a real ` +
+ + +
loading…

: null} {rows && rows.length === 0 ? ( -

+

no agents yet — the swarm-wide identity store has no agents registered on any hive

) : null} - {rows && rows.length > 0 ? ( + {rows && rows.length > 0 && viewMode === "cards" ? (
{rows.map((a) => ( ) : null} + {rows && rows.length > 0 && viewMode === "table" ? ( + a.name} + storageKey="swarm-ui:agents:table-filters" + /> + ) : null} setDetailTarget(null)}