mara, on review: expected the plain Preact pattern (render(h(Widget,
props), container), call again to update) rather than a custom
mountX() returning {refresh(), update()}. Preact's own render is
already the re-render/diff entry point, so the wrapper was indirection
this component didn't need - swarm.js (plain .js, no JSX pragma
required for h()/render() either) now calls render(h(JobqRollup,
{...refreshToken}), root) directly, bumping a module-level token to
force a refetch instead of holding a mount handle.
JobqGraph/mountJobqGraph (a separate, already-merged component) is
untouched - out of scope for this PR, flagged as a possible follow-up
if she wants the same simplification there.
Re-verified: npm run build (whole workspace) + swarm-ui typecheck
clean, comment-block + issue-ref lints clean, re-screenshotted the
dashboard SW4RM tab against the same mocked payload - identical
render, spinner now visibly mid-rotation in the frame (confirms the
animation is live, not just present in markup).
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
// JobqRollup.tsx — <JobqRollup>, a compact one-line banner over
|
|
// GET .../api/jobq/rollup's pre-tallied `Vec<hive_jobq_wire::StateCount>`
|
|
// (any endpoint serving that shape works — hive-c0re and swarm-controller
|
|
// both do). Extracted out of dashboard/src/swarm.js's hand-rolled
|
|
// queue-summary banner, which this component now replaces there too —
|
|
// same "one shared component, not two parallel implementations" pattern
|
|
// `JobqGraph` already established for the rebuild-queue tree view.
|
|
//
|
|
// Reads `roots` (not `nodes`) — "N running / M queued" has always meant
|
|
// *operations*, not raw steps (one rebuild is ~7 nodes but 1 root).
|
|
// `Finishing` counts as running (own work done, subtree still going,
|
|
// still in flight) — the rollup endpoint doesn't collapse the two
|
|
// itself, so this component does. Renders nothing at all when there's
|
|
// no active work, matching the banner it replaces (an idle queue is not
|
|
// worth a line of chrome).
|
|
//
|
|
// `queueHref`, when given, adds a "view queue →" link (dashboard links to
|
|
// /builds.html; swarm-ui's /jobs page omits it — a link to the page
|
|
// you're already on is noise).
|
|
//
|
|
// The glyph carries `.spinner` (`@hive/shared/base.css`, imported by
|
|
// both consumers) for the "actively happening" spin — a static
|
|
// screenshot can't tell a frozen spinner from a missing one.
|
|
//
|
|
// JSX (swarm-ui) — `<JobqRollup endpoint="..." queueHref="..." />`. Or
|
|
// plain `render(h(JobqRollup, props), container)` (dashboard/src/
|
|
// swarm.js, no JSX pragma needed) — call again with a bumped
|
|
// `refreshToken` to force a refetch. No mount wrapper: `render` is
|
|
// already the re-render/diff entry point.
|
|
|
|
import { useState, useEffect } from 'preact/hooks';
|
|
|
|
// Mirrors `hive_jobq_wire::StateSchema` — only the subset this banner
|
|
// cares about, not the full union `JobqGraph.tsx` mirrors, since a
|
|
// rollup row's `state` is read by exact string match, not rendered.
|
|
type NodeState = 'Pending' | 'Running' | 'Finishing' | 'Done' | 'Failed' | 'Cancelled' | 'Skipped';
|
|
|
|
interface StateCount {
|
|
state: NodeState;
|
|
nodes: number;
|
|
roots: number;
|
|
}
|
|
|
|
export interface JobqRollupProps {
|
|
endpoint?: string;
|
|
queueHref?: string;
|
|
refreshToken?: number;
|
|
}
|
|
|
|
export function JobqRollup({ endpoint, queueHref, refreshToken = 0 }: JobqRollupProps) {
|
|
const [counts, setCounts] = useState<StateCount[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!endpoint) return undefined;
|
|
let cancelled = false;
|
|
// Best-effort: a failed fetch leaves the previous snapshot in
|
|
// place rather than wiping the banner on a network blip — same
|
|
// rule the hand-rolled version followed.
|
|
(async () => {
|
|
try {
|
|
const r = await fetch(endpoint);
|
|
if (!r.ok) return;
|
|
const data = (await r.json()) as StateCount[];
|
|
if (!cancelled) setCounts(data);
|
|
} catch {
|
|
// ignore — keep the previous snapshot
|
|
}
|
|
})();
|
|
return () => { cancelled = true; };
|
|
}, [endpoint, refreshToken]);
|
|
|
|
const byState = new Map(counts.map((c) => [c.state, c]));
|
|
const running = (byState.get('Running')?.roots ?? 0) + (byState.get('Finishing')?.roots ?? 0);
|
|
const queued = byState.get('Pending')?.roots ?? 0;
|
|
if (!running && !queued) return null;
|
|
|
|
const parts: string[] = [];
|
|
if (running) parts.push(`${running} running`);
|
|
if (queued) parts.push(`${queued} queued`);
|
|
|
|
return (
|
|
<div class="jqr-summary">
|
|
<span class="jqr-glyph spinner">◐</span>{' '}
|
|
<strong>build queue</strong> — {parts.join(' · ')}{' '}
|
|
{queueHref && (
|
|
<a class="jqr-link" href={queueHref}>view queue →</a>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|