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
|
|
@ -7,6 +7,7 @@
|
|||
referenced from dashboard JS (the schedules view uses `.rqe-source*`),
|
||||
so the dashboard pulls them in here. */
|
||||
@import "./system-sections.css";
|
||||
@import "@hive/shared/jobq-rollup.css";
|
||||
|
||||
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
||||
Top-of-page sticky header with banner + tab strip. SSE stays
|
||||
|
|
@ -456,30 +457,9 @@ hive-agent-menu {
|
|||
|
||||
Notification controls below sit between the banner and the containers. */
|
||||
|
||||
/* Build-queue summary banner on the SW4RM tab: one compact line
|
||||
when the rebuild queue has active work, with a link to the full queue on
|
||||
the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. */
|
||||
.queue-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;
|
||||
}
|
||||
.queue-summary strong { color: var(--amber); }
|
||||
.queue-summary-link {
|
||||
margin-left: auto;
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.queue-summary-link:hover { text-decoration: underline; }
|
||||
/* Build-queue summary banner on the SW4RM tab moved to the shared
|
||||
`JobqRollup` Preact component (`.jqr-*` classes, imported above) —
|
||||
mounted by swarm.js::initJobqRollup into #jobq-rollup-section. */
|
||||
|
||||
/* .notif-row / .btn-notif moved to settings.css with the S3TT1NGS
|
||||
page. */
|
||||
|
|
|
|||
|
|
@ -97,6 +97,13 @@
|
|||
<!-- Swarm / hive identity headline. Populated by refreshState from
|
||||
hive_name + swarm_name; stays hidden when neither is set. -->
|
||||
<h2 id="swarm-identity" hidden></h2>
|
||||
<!-- JobqRollup mount point, kept outside #containers-section
|
||||
deliberately — that section is wiped + rebuilt on every
|
||||
container-list render (see swarm.js::renderContainers), which
|
||||
would tear down and remount the Preact tree on every
|
||||
container-state tick. Mounted once by swarm.js::initJobqRollup,
|
||||
refreshed via its own handle rather than by re-rendering. -->
|
||||
<div id="jobq-rollup-section"></div>
|
||||
<div id="containers-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from './common.js';
|
||||
import { el } from '@hive/shared/dom.js';
|
||||
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
|
||||
import { mountJobqRollup } from '@hive/shared/jobq-rollup.js';
|
||||
import {
|
||||
containersState, questionsState,
|
||||
} from './state.js';
|
||||
|
|
@ -62,35 +63,40 @@ const selectionState = new Set();
|
|||
// a card's pending badges are transients-only now, which already means
|
||||
// "what is running") and this banner, which was pulled entirely per "dont
|
||||
// replace one legacy thing with another" (a client-side tally over the
|
||||
// generic graph was itself judged a stopgap). Now that the dedicated
|
||||
// rollup endpoint exists (hive-jobq-wire::state_rollup, served at
|
||||
// GET /api/jobq/rollup), the banner reads *that* instead — a handful of
|
||||
// pre-tallied counts, not the graph.
|
||||
let jobqRollupState = [];
|
||||
// generic graph was itself judged a stopgap). Rendering itself later
|
||||
// moved out to the shared `JobqRollup` Preact component (same one
|
||||
// swarm-ui's /jobs page mounts), which owns its own fetch of
|
||||
// GET /api/jobq/rollup — this file just mounts it once and bumps its
|
||||
// refresh handle, mirroring builds.js's JobqGraph mount exactly.
|
||||
//
|
||||
// Mounted into #jobq-rollup-section, a sibling of #containers-section
|
||||
// kept OUTSIDE that section's per-render `replaceChildren()` wipe (see
|
||||
// dashboard.html's comment on the mount div) — re-mounting a fresh
|
||||
// Preact tree on every container-state tick would work but is wasteful
|
||||
// and defeats the component owning its own fetch lifecycle.
|
||||
let jobqRollupHandle = null;
|
||||
|
||||
// Fetches the rollup fresh and re-renders. Called on cold load (see
|
||||
// tabs.js's refreshState) and whenever `rebuild_queue_changed` fires
|
||||
// (applyRebuildQueueChanged below) — a payload-less push trigger by
|
||||
// Mounts once; a second call is a no-op (idempotent — matches
|
||||
// `initCall`/`initPermissions`'s "safe to call from cold-load every
|
||||
// time" shape elsewhere in this bundle).
|
||||
export function initJobqRollup() {
|
||||
if (jobqRollupHandle) return;
|
||||
const root = $('jobq-rollup-section');
|
||||
if (!root) return;
|
||||
jobqRollupHandle = mountJobqRollup(root, {
|
||||
endpoint: '/api/jobq/rollup',
|
||||
queueHref: '/builds.html',
|
||||
});
|
||||
}
|
||||
// Refetches on cold load (tabs.js's refreshState) and whenever
|
||||
// `rebuild_queue_changed` fires — a payload-less push trigger by
|
||||
// design, confirmed with atlas on the jobq-deletion tracker: the event
|
||||
// carries no `queue` field this page reads, same "something changed,
|
||||
// go refetch" treatment builds.js already gives its JobqGraph mount
|
||||
// go refetch" treatment builds.js already gives its own JobqGraph mount
|
||||
// handle's .refresh().
|
||||
// Best-effort: a failed fetch leaves the previous snapshot in place
|
||||
// rather than wiping the banner on a network blip.
|
||||
export async function refreshJobqRollup() {
|
||||
let counts;
|
||||
try {
|
||||
const r = await fetch('/api/jobq/rollup');
|
||||
if (!r.ok) return;
|
||||
counts = await r.json();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
jobqRollupState = counts;
|
||||
renderContainersFromState();
|
||||
}
|
||||
export function applyRebuildQueueChanged() {
|
||||
refreshJobqRollup();
|
||||
initJobqRollup();
|
||||
jobqRollupHandle?.refresh();
|
||||
}
|
||||
|
||||
// ─── transients ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -627,31 +633,9 @@ export function renderContainers(s) {
|
|||
));
|
||||
}
|
||||
|
||||
// Queue-summary banner: one compact line above the container list when
|
||||
// the job queue has active work, linking to the full queue on the
|
||||
// BU1LDS page. Reads GET /api/jobq/rollup's pre-tallied `roots` counts
|
||||
// (see jobqRollupState above) rather than the full graph — `roots`
|
||||
// because "N running / M queued" has always meant *operations*, not
|
||||
// raw steps (one rebuild is ~7 nodes but 1 root); `nodes` exists on
|
||||
// the same endpoint for a consumer that wants steps instead, unused
|
||||
// here. `Finishing` counts as running (own work done, subtree still
|
||||
// going, still in flight) — same treatment `roots` gets nowhere else,
|
||||
// since the rollup endpoint doesn't collapse the two itself.
|
||||
const byState = new Map(jobqRollupState.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) {
|
||||
const parts = [];
|
||||
if (running) parts.push(`${running} running`);
|
||||
if (queued) parts.push(`${queued} queued`);
|
||||
root.append(el('div', { class: 'queue-summary' },
|
||||
el('span', { class: 'glyph spinner' }, '◐'), ' ',
|
||||
el('strong', {}, 'build queue'), ' — ',
|
||||
parts.join(' · '), ' ',
|
||||
el('a', { class: 'queue-summary-link', href: '/builds.html' }, 'view queue →'),
|
||||
));
|
||||
}
|
||||
// Queue-summary banner lives outside this section now — see
|
||||
// #jobq-rollup-section / initJobqRollup, mounted once rather than
|
||||
// rebuilt on every render this function does.
|
||||
|
||||
if (!containers.length && !transientsState.size) {
|
||||
root.append(el('p', { class: 'empty' }, 'no managed containers'));
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import {
|
|||
renderQuestions, activeQuestionCount,
|
||||
} from './call.js';
|
||||
import {
|
||||
refreshJobqRollup, syncTransientsFromSnapshot,
|
||||
initJobqRollup, syncTransientsFromSnapshot,
|
||||
applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved,
|
||||
applyTransientSet, applyTransientCleared,
|
||||
renderContainers, renderContainersFromState,
|
||||
|
|
@ -232,13 +232,12 @@ window.marked = marked;
|
|||
syncTransientsFromSnapshot(s);
|
||||
syncContainersFromSnapshot(s);
|
||||
// Job-queue rollup feeds only the SW4RM queue-summary banner
|
||||
// (per-agent card badges are transient-only — see swarm.js).
|
||||
// Self-fetches GET /api/jobq/rollup — not read off `s` (this
|
||||
// page's snapshot carries no jobq field) — fire-and-forget:
|
||||
// renderContainers below runs off whatever jobqRollupState
|
||||
// already holds, and refreshJobqRollup's own re-render catches
|
||||
// up once the fetch resolves.
|
||||
refreshJobqRollup();
|
||||
// (per-agent card badges are transient-only — see swarm.js). Its
|
||||
// own `JobqRollup` mount self-fetches GET /api/jobq/rollup — not
|
||||
// read off `s` (this page's snapshot carries no jobq field).
|
||||
// initJobqRollup mounts once and is a no-op on later calls; the
|
||||
// mount's own effect handles the actual fetch.
|
||||
initJobqRollup();
|
||||
renderContainers(s);
|
||||
// Sync the derived approvals + questions stores from the
|
||||
// snapshot, then render. Live `*_added` / `*_resolved` events
|
||||
|
|
|
|||
Loading…
Reference in a new issue