swarm-ui: the hives card and the alerts table show placeholder rows under their own header while they load
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.
This commit is contained in:
parent
33c230665b
commit
e149ff3315
5 changed files with 54 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<string, BadgeTone> = {
|
||||
|
|
@ -111,7 +112,9 @@ function AlertsBody() {
|
|||
problem={error}
|
||||
/>
|
||||
) : null}
|
||||
{!error && alerts === null ? <p>loading…</p> : null}
|
||||
{!error && alerts === null ? (
|
||||
<TableSkeleton columns={COLUMNS} label="the alerts" />
|
||||
) : null}
|
||||
{listed && listed.length === 0 ? (
|
||||
<p>{showAll ? "no alerts." : "nothing is alerting."}</p>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<AgentListSkeleton count={2} />
|
||||
</SkeletonRegion>
|
||||
</Sample>
|
||||
<Sample label="table placeholder">
|
||||
<TableSkeleton columns={TABLE_COLUMNS} label="the rows" />
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="MultiselectFilter">
|
||||
|
|
|
|||
|
|
@ -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 ? <p>loading…</p> : null}
|
||||
{!error && hives === null ? (
|
||||
<TableSkeleton columns={COLUMNS} label="the hives" />
|
||||
) : null}
|
||||
{hives ? (
|
||||
<Table
|
||||
columns={COLUMNS}
|
||||
|
|
|
|||
39
frontend/packages/swarm-ui/src/ui/table/TableSkeleton.tsx
Normal file
39
frontend/packages/swarm-ui/src/ui/table/TableSkeleton.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// <TableSkeleton> — the twin of <Table> for a view that is waiting on its rows:
|
||||
// the same header cells (the page passes the `columns` it gives its <Table>)
|
||||
// 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<T>({
|
||||
columns,
|
||||
label,
|
||||
rows = 3,
|
||||
}: {
|
||||
columns: TableColumn<T>[];
|
||||
/** What is loading, as a screen reader says it: "the hives". */
|
||||
label: string;
|
||||
rows?: number;
|
||||
}) {
|
||||
const shapes: TableColumn<number>[] = columns.map((c) => ({
|
||||
key: c.key,
|
||||
header: c.header,
|
||||
render: () => <Skeleton width="70%" height="1.4em" />,
|
||||
}));
|
||||
return (
|
||||
<SkeletonRegion label={label}>
|
||||
<div aria-hidden="true">
|
||||
<Table
|
||||
columns={shapes}
|
||||
rows={Array.from({ length: rows }, (_, i) => i)}
|
||||
rowKey={String}
|
||||
storageKey="swarm-ui:table-skeleton"
|
||||
/>
|
||||
</div>
|
||||
</SkeletonRegion>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue