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:
parent
f276ee342c
commit
bb789e4603
3 changed files with 48 additions and 3 deletions
|
|
@ -583,7 +583,8 @@ previous repo's issues under the error.
|
||||||
|
|
||||||
### Builds
|
### Builds
|
||||||
|
|
||||||
The builds sit in a card under the job graph on the **jobs** tab. Both cards start
|
The builds sit in a card under the job graph on the **jobs** tab. While the graph
|
||||||
|
loads for the first time, it shows grey placeholder rows in the shape of a job tree. Both cards start
|
||||||
shut, and each reads nothing until you open it; a link to
|
shut, and each reads nothing until you open it; a link to
|
||||||
a build opens the builds card. The old `/builds` address redirects there and
|
a build opens the builds card. The old `/builds` address redirects there and
|
||||||
keeps its query.
|
keeps its query.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
// loader is `text` (for unrelated shadow-DOM components' CSS-as-string
|
// loader is `text` (for unrelated shadow-DOM components' CSS-as-string
|
||||||
// needs), and that loader is global per call, not per-module.
|
// needs), and that loader is global per call, not per-module.
|
||||||
|
|
||||||
|
import type { ComponentChildren } from "preact";
|
||||||
import { useState, useEffect, useCallback, useRef } from "preact/hooks";
|
import { useState, useEffect, useCallback, useRef } from "preact/hooks";
|
||||||
|
|
||||||
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
|
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
|
||||||
|
|
@ -283,6 +284,9 @@ export interface JobqGraphProps {
|
||||||
onUpdate?: (nodes: GraphNode[]) => void;
|
onUpdate?: (nodes: GraphNode[]) => void;
|
||||||
onCancel?: (id: number) => void;
|
onCancel?: (id: number) => void;
|
||||||
refreshToken?: number;
|
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
|
// `refreshToken` is not read anywhere in the body — its only job is to
|
||||||
|
|
@ -295,6 +299,7 @@ export function JobqGraph({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onCancel,
|
onCancel,
|
||||||
refreshToken = 0,
|
refreshToken = 0,
|
||||||
|
loading,
|
||||||
}: JobqGraphProps) {
|
}: JobqGraphProps) {
|
||||||
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
|
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
|
||||||
() => new Set(ALL_STATES.filter((s) => !DEFAULT_HIDDEN_STATES.has(s))),
|
() => new Set(ALL_STATES.filter((s) => !DEFAULT_HIDDEN_STATES.has(s))),
|
||||||
|
|
@ -344,7 +349,7 @@ export function JobqGraph({
|
||||||
{error ? (
|
{error ? (
|
||||||
<p class="jg-error-msg">fetch failed: {error}</p>
|
<p class="jg-error-msg">fetch failed: {error}</p>
|
||||||
) : nodes === null ? (
|
) : nodes === null ? (
|
||||||
<p class="jg-empty">loading…</p>
|
(loading ?? <p class="jg-empty">loading…</p>)
|
||||||
) : !nodes.length ? (
|
) : !nodes.length ? (
|
||||||
<p class="jg-empty">empty</p>
|
<p class="jg-empty">empty</p>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,12 @@
|
||||||
// fetch — both `JobqGraph`/`JobqRollup` already accept that prop
|
// fetch — both `JobqGraph`/`JobqRollup` already accept that prop
|
||||||
// (bumping it is their documented "refetch now" lever) since the
|
// (bumping it is their documented "refetch now" lever) since the
|
||||||
// dashboard's own rebuild-queue view needed the same thing first.
|
// dashboard's own rebuild-queue view needed the same thing first.
|
||||||
|
import type { ComponentChildren } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
||||||
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
|
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
|
||||||
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
|
||||||
|
import { Skeleton, SkeletonRegion } from "../ui/skeleton/Skeleton.js";
|
||||||
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
import { Toolbar } from "../ui/toolbar/Toolbar.js";
|
||||||
import { BuildsCard } from "./builds/BuildsPage.js";
|
import { BuildsCard } from "./builds/BuildsPage.js";
|
||||||
import { ReviewPage } from "./review/ReviewPage.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.
|
// just solve staleness rather than require an opt-in every visit.
|
||||||
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
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() {
|
function JobsBody() {
|
||||||
const [intervalMs, setIntervalMs] =
|
const [intervalMs, setIntervalMs] =
|
||||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||||
|
|
@ -62,7 +97,11 @@ function JobsBody() {
|
||||||
/>
|
/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<JobqRollup endpoint="/api/jobq/rollup" refreshToken={refreshToken} />
|
<JobqRollup endpoint="/api/jobq/rollup" refreshToken={refreshToken} />
|
||||||
<JobqGraph endpoint="/api/jobq/graph" refreshToken={refreshToken} />
|
<JobqGraph
|
||||||
|
endpoint="/api/jobq/graph"
|
||||||
|
refreshToken={refreshToken}
|
||||||
|
loading={<JobsGraphSkeleton />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue