Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the job graph shows placeholder rows in the shape of a job tree while it loads

JobqGraph takes an optional loading element, so the dashboard keeps its text
and the swarm UI's jobs tab passes a placeholder built from the graph's own
node and row classes: a root with two children and a second root, with the
real indent and rail.
This commit is contained in:
iris 2026-11-06 20:18:34 +01:00 • committed by mara
commit bb789e4603
2 changed files with 48 additions and 3 deletions

View file

@ -28,6 +28,7 @@
// loader is `text` (for unrelated shadow-DOM components' CSS-as-string
// needs), and that loader is global per call, not per-module.
import type { ComponentChildren } from "preact";
import { useState, useEffect, useCallback, useRef } from "preact/hooks";
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
@ -283,6 +284,9 @@ export interface JobqGraphProps {
onUpdate?: (nodes: GraphNode[]) => void;
onCancel?: (id: number) => void;
refreshToken?: number;
/** What shows while the first answer loads. A host that has a placeholder in
* the shape of its graph passes it; the default is a line of text. */
loading?: ComponentChildren;
}
// `refreshToken` is not read anywhere in the body — its only job is to
@ -295,6 +299,7 @@ export function JobqGraph({
onUpdate,
onCancel,
refreshToken = 0,
loading,
}: JobqGraphProps) {
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
() => new Set(ALL_STATES.filter((s) => !DEFAULT_HIDDEN_STATES.has(s))),
@ -344,7 +349,7 @@ export function JobqGraph({
{error ? (
<p class="jg-error-msg">fetch failed: {error}</p>
) : nodes === null ? (
<p class="jg-empty">loading…</p>
(loading ?? <p class="jg-empty">loading…</p>)
) : !nodes.length ? (
<p class="jg-empty">empty</p>
) : (

View file

@ -18,10 +18,12 @@
// fetch — both `JobqGraph`/`JobqRollup` already accept that prop
// (bumping it is their documented "refetch now" lever) since the
// dashboard's own rebuild-queue view needed the same thing first.
import type { ComponentChildren } from "preact";
import { useState } from "preact/hooks";
import { JobqGraph } from "@hive/shared/jobq-graph.js";
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { Skeleton, SkeletonRegion } from "../ui/skeleton/Skeleton.js";
import { Toolbar } from "../ui/toolbar/Toolbar.js";
import { BuildsCard } from "./builds/BuildsPage.js";
import { ReviewPage } from "./review/ReviewPage.js";
@ -37,6 +39,39 @@ import "./JobsPage.css";
// just solve staleness rather than require an opt-in every visit.
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
// The graph's twin while it loads: the graph's own node and row classes, so the
// rows and the indent of the children are the real ones, with a state glyph and a
// label as placeholders. Two roots, the first with two children.
function SkeletonNode({
label,
children,
}: {
label: string;
children?: ComponentChildren;
}) {
return (
<div class="jg-node">
<div class="jg-row">
<Skeleton shape="circle" width="1em" height="1em" />
<Skeleton width={label} />
</div>
{children}
</div>
);
}
function JobsGraphSkeleton() {
return (
<SkeletonRegion label="the jobs">
<SkeletonNode label="14em">
<SkeletonNode label="10em" />
<SkeletonNode label="12em" />
</SkeletonNode>
<SkeletonNode label="9em" />
</SkeletonRegion>
);
}
function JobsBody() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
@ -62,7 +97,11 @@ function JobsBody() {
/>
</Toolbar>
<JobqRollup endpoint="/api/jobq/rollup" refreshToken={refreshToken} />
<JobqGraph endpoint="/api/jobq/graph" refreshToken={refreshToken} />
<JobqGraph
endpoint="/api/jobq/graph"
refreshToken={refreshToken}
loading={<JobsGraphSkeleton />}
/>
</>
);
}