From e149ff3315fd92e5fc278e657e499bc1de8eca7d Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 6 Nov 2026 00:50:26 +0100 Subject: [PATCH] swarm-ui: the hives card and the alerts table show placeholder rows under their own header while they load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TableSkeleton is the twin of Table: the same columns, so the same header cells, with a placeholder shape in each cell of three blank rows and no sort or filter, which have nothing to work on yet. The hives card and the alerts table use it in place of their "loading…" text. --- docs/swarm/ui.md | 4 +- .../swarm-ui/src/pages/AlertsPage.tsx | 5 ++- .../swarm-ui/src/pages/ComponentsPage.tsx | 4 ++ .../swarm-ui/src/pages/swarm/HivesCard.tsx | 5 ++- .../swarm-ui/src/ui/table/TableSkeleton.tsx | 39 +++++++++++++++++++ 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 frontend/packages/swarm-ui/src/ui/table/TableSkeleton.tsx diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index f6a7fdcd..3a223f62 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -44,7 +44,9 @@ The Jobs page has none yet. The pages that only display (agents, alerts, builds, review runs and the hives card) show the answer they last saw at once, then replace it with the fresh one -while the spinner runs, so opening one never starts on an empty page. The browser +while the spinner runs, so opening one never starts on an empty page. On the very +first visit, the hives card and the alerts table show their own header with three +grey placeholder rows until the first answer comes. The browser keeps the answer for itself only, and forgets it after a day, after a new build of the UI, at **log out**, and when the login ends. The pages with an editor (schedules, notification flows, broadcast) start empty as before. diff --git a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx index d7bfc4fe..31bba464 100644 --- a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx @@ -15,6 +15,7 @@ import { } from "../ui/refresh-interval/RefreshInterval.js"; import { fetchJson, usePolled } from "../ui/polled/usePolled.js"; import { Table, type TableColumn } from "../ui/table/Table.js"; +import { TableSkeleton } from "../ui/table/TableSkeleton.js"; import { activeSinceMs, alertKey, listedAlerts, type Alert } from "./alerts.js"; const STATE_TONE: Record = { @@ -111,7 +112,9 @@ function AlertsBody() { problem={error} /> ) : null} - {!error && alerts === null ?

loading…

: null} + {!error && alerts === null ? ( + + ) : null} {listed && listed.length === 0 ? (

{showAll ? "no alerts." : "nothing is alerting."}

) : null} diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 50e1bd19..33a844d8 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -23,6 +23,7 @@ import { } from "../ui/refresh-interval/RefreshInterval.js"; import { SplitView } from "../ui/split-view/SplitView.js"; import { Table, type TableColumn } from "../ui/table/Table.js"; +import { TableSkeleton } from "../ui/table/TableSkeleton.js"; import { TextField } from "../ui/text-field/TextField.js"; import { TextAreaField } from "../ui/text-area-field/TextAreaField.js"; import { @@ -491,6 +492,9 @@ export function ComponentsPage() { + + +
diff --git a/frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx b/frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx index 6d763db2..2d6bc8b5 100644 --- a/frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx +++ b/frontend/packages/swarm-ui/src/pages/swarm/HivesCard.tsx @@ -24,6 +24,7 @@ import { } from "../../ui/refresh-interval/RefreshInterval.js"; import { fetchJson, usePolled } from "../../ui/polled/usePolled.js"; import { Table, type TableColumn } from "../../ui/table/Table.js"; +import { TableSkeleton } from "../../ui/table/TableSkeleton.js"; import { domainCell } from "../hive-domain.js"; type Freshness = "fresh" | "stale" | "never_reported" | "unknown"; @@ -142,7 +143,9 @@ function HiveRoster() { problem={error} /> ) : null} - {!error && hives === null ?

loading…

: null} + {!error && hives === null ? ( + + ) : null} {hives ? ( — the twin of
for a view that is waiting on its rows: +// the same header cells (the page passes the `columns` it gives its
) +// over a few blank rows, with a placeholder shape in each cell instead of the +// real cell. Sort and filter are left out: there is nothing to sort or filter +// yet. The overall width matches, but the width of each column does not: a +// table sizes its columns from their content, which is not known yet, and the +// real header also carries sort and filter controls. The table is hidden from +// assistive tech; the region's label says that rows are loading. +import { Skeleton, SkeletonRegion } from "../skeleton/Skeleton.js"; +import { Table, type TableColumn } from "./Table.js"; + +export function TableSkeleton({ + columns, + label, + rows = 3, +}: { + columns: TableColumn[]; + /** What is loading, as a screen reader says it: "the hives". */ + label: string; + rows?: number; +}) { + const shapes: TableColumn[] = columns.map((c) => ({ + key: c.key, + header: c.header, + render: () => , + })); + return ( + +
i)} + rowKey={String} + storageKey="swarm-ui:table-skeleton" + /> + + + ); +}