extract build-queue rollup as a shared Preact component
New @hive/shared/jobq-rollup.js (JobqRollup.tsx + jobq-rollup.css), mirroring JobqGraph's shape exactly: JSX use plus an imperative mountJobqRollup(container, props) for a plain-.js call site. Fetches Vec<hive_jobq_wire::StateCount> off `endpoint`, sums Running+Finishing roots as "running" and Pending roots as "queued", renders nothing when both are zero. Optional `queueHref` adds a "view queue -> " link. Swapped dashboard's hand-rolled queue-summary banner (swarm.js) over to this component instead of keeping two parallel implementations - same "one shared component" pattern JobqGraph already set for the rebuild queue tree view. Mounted once into a new #jobq-rollup-section, kept as a sibling of (not inside) #containers-section since that section gets replaceChildren()-wiped on every container-state render, which would tear down and remount a Preact tree on every tick. Refreshed via the mount handle's .refresh() on rebuild_queue_changed, same as builds.js's JobqGraph handle. Also mounted in swarm-ui's /jobs page, above JobqGraph, with no queueHref (a link back to the page you're already on is noise) - the literal ask on hyperhive#3364. Verified: npm run build (whole workspace) and swarm-ui typecheck both clean, comment-block + issue-ref lints run manually, headless-chromium screenshots of both the dashboard SW4RM tab and swarm-ui's /jobs page against mocked /api/jobq/rollup payloads - banner renders identically in both, with and without the queue link as expected.
This commit is contained in:
parent
9451c06e82
commit
1111577c91
10 changed files with 208 additions and 99 deletions
100
frontend/packages/shared/src/jobq-rollup/JobqRollup.tsx
Normal file
100
frontend/packages/shared/src/jobq-rollup/JobqRollup.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// 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).
|
||||
//
|
||||
// Two ways to use this, same shape as JobqGraph: JSX (swarm-ui) —
|
||||
// `<JobqRollup endpoint="..." queueHref="..." />`. Or imperative mount
|
||||
// (dashboard/src/swarm.js, plain `.js`) — `mountJobqRollup(container,
|
||||
// props)` returns `{ refresh(), update(props) }`.
|
||||
|
||||
import { h, render } from 'preact';
|
||||
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">◐</span>{' '}
|
||||
<strong>build queue</strong> — {parts.join(' · ')}{' '}
|
||||
{queueHref && (
|
||||
<a class="jqr-link" href={queueHref}>view queue →</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Imperative mount helper — same shape as `mountJobqGraph`. Returns
|
||||
// `.refresh()` (bump the refresh token, re-fetch) and `.update(props)`
|
||||
// (merge new props and re-render).
|
||||
export function mountJobqRollup(container: Element, initialProps: JobqRollupProps) {
|
||||
let props = initialProps;
|
||||
let token = 0;
|
||||
const draw = () => render(h(JobqRollup, { ...props, refreshToken: token }), container);
|
||||
draw();
|
||||
return {
|
||||
refresh() { token += 1; draw(); },
|
||||
update(next: Partial<JobqRollupProps>) { props = { ...props, ...next }; draw(); },
|
||||
};
|
||||
}
|
||||
25
frontend/packages/shared/src/jobq-rollup/jobq-rollup.css
Normal file
25
frontend/packages/shared/src/jobq-rollup/jobq-rollup.css
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* JobqRollup.tsx styles — light DOM, `.jqr-*` prefixed to avoid
|
||||
colliding with a host page's own classes (same convention
|
||||
jobq-graph.css uses with `.jg-*`). Amber to match the in-progress /
|
||||
"rebuilding" card tint dashboard.css already uses elsewhere. */
|
||||
.jqr-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
flex-wrap: wrap;
|
||||
background: color-mix(in srgb, var(--amber) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 55%, transparent);
|
||||
color: var(--amber);
|
||||
padding: 0.45em 0.8em;
|
||||
margin-bottom: 0.6em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.jqr-summary strong { color: var(--amber); }
|
||||
.jqr-link {
|
||||
margin-left: auto;
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.jqr-link:hover { text-decoration: underline; }
|
||||
Loading…
Reference in a new issue