swarm-ui: the schedules, notification flows and flow editor show placeholders while they load
The schedules card, the notification router card (flows and recent deliveries) and the flow editor showed a bare 'loading…' line. They now show grey placeholders in the layout of what is coming: schedule cards with the real card's row heights, flow cards with a block for the graph, the deliveries table's header over blank rows, and lines for the editor.
This commit is contained in:
parent
0bc0b86d1d
commit
5eef1f4a3d
7 changed files with 111 additions and 7 deletions
|
|
@ -4,6 +4,7 @@
|
|||
import { EmptyState } from "../../ui/empty-state/EmptyState.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||
import { TableSkeleton } from "../../ui/table/TableSkeleton.js";
|
||||
import { issueRef, outcomeText, type Delivery } from "./RouterLive.js";
|
||||
|
||||
// A delivery with its place in the answer, which is what tells two identical rows
|
||||
|
|
@ -48,6 +49,11 @@ const COLUMNS: TableColumn<Row>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
/** The table's twin while the deliveries load. */
|
||||
export function DeliveriesSkeleton() {
|
||||
return <TableSkeleton columns={COLUMNS} label="the deliveries" />;
|
||||
}
|
||||
|
||||
export function Deliveries({ rows }: { rows: Delivery[] }) {
|
||||
if (rows.length === 0) return <EmptyState>no deliveries yet</EmptyState>;
|
||||
const placed = rows.map((d, place) => ({ ...d, place }));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
|||
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
||||
import { Hint } from "../../ui/hint/Hint.js";
|
||||
import { Panel } from "../../ui/panel/Panel.js";
|
||||
import { SkeletonLines } from "../../ui/skeleton/Skeleton.js";
|
||||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
import { fetchJson, ProblemError } from "../../ui/polled/usePolled.js";
|
||||
import { FlowCanvas } from "./FlowCanvas.js";
|
||||
|
|
@ -248,7 +249,7 @@ export function FlowEditorPage({
|
|||
if (!view) {
|
||||
return (
|
||||
<Panel title="notification flow" icon="🔔" actions={back}>
|
||||
<p>loading…</p>
|
||||
<SkeletonLines label="the flow" />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// The twin of the flow cards for a list that is loading: the same card and
|
||||
// head layout (`notify-flow-head`), a placeholder for the name and the
|
||||
// buttons, and a block where the flow's graph goes. The list is in the
|
||||
// container the real one uses (`notify-flows`), so the gap is the same.
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js";
|
||||
|
||||
function FlowCardSkeleton() {
|
||||
return (
|
||||
<Card>
|
||||
<div class="notify-flow-head">
|
||||
<Skeleton width="12em" class="notify-flow-name" />
|
||||
<Skeleton width="9em" height="2.4em" />
|
||||
</div>
|
||||
<Skeleton shape="block" width="100%" height="6em" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlowListSkeleton({ count = 3 }: { count?: number }) {
|
||||
return (
|
||||
<SkeletonRegion label="the flows">
|
||||
<div class="notify-flows">
|
||||
{Array.from({ length: count }, (_, i) => (
|
||||
<FlowCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</SkeletonRegion>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,7 +20,8 @@ import {
|
|||
type RefreshIntervalMs,
|
||||
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import { Deliveries } from "./Deliveries.js";
|
||||
import { Deliveries, DeliveriesSkeleton } from "./Deliveries.js";
|
||||
import { FlowListSkeleton } from "./FlowListSkeleton.js";
|
||||
import { NEW_FLOW_PATH } from "./FlowDraft.js";
|
||||
import { FlowGraph } from "./FlowGraph.js";
|
||||
import { withBuiltin, type FlowsView } from "./FlowTypes.js";
|
||||
|
|
@ -124,7 +125,7 @@ export function NotificationRouter() {
|
|||
problem={actionError}
|
||||
/>
|
||||
) : null}
|
||||
{!error && view === null ? <p>loading…</p> : null}
|
||||
{!error && view === null ? <FlowListSkeleton /> : null}
|
||||
{view ? (
|
||||
<div class="notify-flows">
|
||||
{dryError ? (
|
||||
|
|
@ -191,7 +192,7 @@ export function NotificationRouter() {
|
|||
) : deliveries ? (
|
||||
<Deliveries rows={deliveries.rows} />
|
||||
) : (
|
||||
<p>loading…</p>
|
||||
<DeliveriesSkeleton />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -31,3 +31,19 @@
|
|||
.ui-schedule-card-actions {
|
||||
gap: 0.5em;
|
||||
}
|
||||
/* The loading twin's list: the gap between cards in the real list. */
|
||||
.ui-schedule-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
/* The loading twin's rows take the height of a line of text, and its head the
|
||||
height of the title, so the card is the size of a real one. */
|
||||
.ui-schedule-card-head-skeleton {
|
||||
min-height: 1.76em;
|
||||
}
|
||||
.ui-schedule-card-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 1.6em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ActionRow } from "../../ui/action-row/ActionRow.js";
|
|||
import { Button } from "../../ui/button/Button.js";
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js";
|
||||
import {
|
||||
fmtInterval,
|
||||
fmtLocal,
|
||||
|
|
@ -87,3 +88,49 @@ export function ScheduleCard({
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// The twin of `ScheduleCard` for a list that is loading: the same card and
|
||||
// layout classes, with a placeholder in each slot. The cards sit in the
|
||||
// container the real list uses, so the gap between them is the same.
|
||||
function ScheduleCardSkeleton() {
|
||||
return (
|
||||
<div>
|
||||
<Card class="ui-schedule-card">
|
||||
<div class="ui-schedule-card-head ui-schedule-card-head-skeleton">
|
||||
<Skeleton width="10em" height="1.2em" class="ui-schedule-card-name" />
|
||||
<Skeleton width="4em" height="1.6em" />
|
||||
<Skeleton width="6em" height="1.6em" />
|
||||
</div>
|
||||
<div class="ui-schedule-card-line">
|
||||
<Skeleton width="14em" />
|
||||
</div>
|
||||
<div class="ui-schedule-card-line">
|
||||
<Skeleton width="90%" />
|
||||
</div>
|
||||
<div class="ui-schedule-card-line">
|
||||
<Skeleton width="60%" />
|
||||
</div>
|
||||
<div class="ui-schedule-card-line">
|
||||
<Skeleton width="12em" />
|
||||
</div>
|
||||
<ActionRow align="end" class="ui-schedule-card-actions">
|
||||
<Skeleton width="4em" height="2.75em" />
|
||||
<Skeleton width="4.5em" height="2.75em" />
|
||||
<Skeleton width="5em" height="2.75em" />
|
||||
</ActionRow>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScheduleListSkeleton({ count = 3 }: { count?: number }) {
|
||||
return (
|
||||
<SkeletonRegion label="the schedules">
|
||||
<div class="ui-schedule-card-list">
|
||||
{Array.from({ length: count }, (_, i) => (
|
||||
<ScheduleCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</SkeletonRegion>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||
import type { TableColumn } from "../../ui/table/Table.js";
|
||||
import type { AgentRow } from "../../agent/AgentTypes.js";
|
||||
import { ScheduleCard } from "./ScheduleCard.js";
|
||||
import { ScheduleCard, ScheduleListSkeleton } from "./ScheduleCard.js";
|
||||
import { agentNamesFrom, agentOptionsFor } from "./agent-options.js";
|
||||
import { ScheduleForm } from "./ScheduleForm.js";
|
||||
import { scheduleState, type Schedule } from "./ScheduleTypes.js";
|
||||
|
|
@ -184,7 +184,7 @@ function SchedulesBody() {
|
|||
onCancel={() => setCreating(false)}
|
||||
/>
|
||||
) : null}
|
||||
{!error && schedules === null ? <p>loading…</p> : null}
|
||||
{!error && schedules === null ? <ScheduleListSkeleton /> : null}
|
||||
{schedules ? (
|
||||
<FilterableView
|
||||
columns={COLUMNS}
|
||||
|
|
|
|||
Loading…
Reference in a new issue