// JobqRollup.tsx — , a compact one-line banner over // GET .../api/jobq/rollup's pre-tallied `Vec` // (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) — ``. 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([]); 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 (
{' '} build queue — {parts.join(' · ')}{' '} {queueHref && ( view queue → )}
); }