Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
075d1f63e3 | ||
|
|
5c9d89dc64 | ||
|
|
348b2f96da | ||
|
|
1111577c91 |
12 changed files with 233 additions and 137 deletions
|
|
@ -986,17 +986,21 @@ agent is stale. Banner pulses on each broker SSE event
|
||||||
**Build-queue summary banner** — when the job queue has any active
|
**Build-queue summary banner** — when the job queue has any active
|
||||||
work, a compact amber banner sits above the container list: `◐ build
|
work, a compact amber banner sits above the container list: `◐ build
|
||||||
queue — N running · M queued — view queue →` (the link goes to the
|
queue — N running · M queued — view queue →` (the link goes to the
|
||||||
BU1LDS page's R3BU1LD QU3U3). Reads `GET /api/jobq/rollup`
|
BU1LDS page's R3BU1LD QU3U3). The shared `JobqRollup` Preact component
|
||||||
(`hive-jobq-wire::state_rollup`, `jobqRollupState` in `swarm.js`) —
|
(`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
|
||||||
`Vec<{ state, nodes, roots }>`, every lifecycle state present in a
|
mounts, pointed at swarm-controller's own rollup endpoint instead),
|
||||||
fixed order, zero counts included — rather than the full
|
mounted once into `#jobq-rollup-section` by `swarm.js::initJobqRollup`
|
||||||
`/api/jobq/graph` tree: `running` sums the `Running` and `Finishing`
|
and refreshed via its own handle rather than being re-rendered by
|
||||||
entries' `roots` (`Finishing` = own work done, subtree still going,
|
`renderContainers`. Reads `GET /api/jobq/rollup`
|
||||||
still in flight), `queued` reads the `Pending` entry's `roots`.
|
(`hive-jobq-wire::state_rollup`) — `Vec<{ state, nodes, roots }>`,
|
||||||
`roots` specifically, not `nodes` — the banner means *N whole
|
every lifecycle state present in a fixed order, zero counts included —
|
||||||
operations*, not raw steps (one rebuild is ~7 nodes but 1 root);
|
rather than the full `/api/jobq/graph` tree: `running` sums the
|
||||||
`nodes` exists on the same endpoint for a consumer that wants
|
`Running` and `Finishing` entries' `roots` (`Finishing` = own work
|
||||||
step-level counts instead, unused here.
|
done, subtree still going, still in flight), `queued` reads the
|
||||||
|
`Pending` entry's `roots`. `roots` specifically, not `nodes` — the
|
||||||
|
banner means *N whole operations*, not raw steps (one rebuild is ~7
|
||||||
|
nodes but 1 root); `nodes` exists on the same endpoint for a consumer
|
||||||
|
that wants step-level counts instead, unused here.
|
||||||
|
|
||||||
### Themed dialogs
|
### Themed dialogs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ import { $, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings } fro
|
||||||
import { el } from '@hive/shared/dom.js';
|
import { el } from '@hive/shared/dom.js';
|
||||||
import { bindAsyncForms } from '@hive/shared/forms.js';
|
import { bindAsyncForms } from '@hive/shared/forms.js';
|
||||||
import { themedConfirm } from '@hive/shared/modal.js';
|
import { themedConfirm } from '@hive/shared/modal.js';
|
||||||
|
import { h, render } from 'preact';
|
||||||
import { fmtAgo, fmtDuration, truncate } from './util.js';
|
import { fmtAgo, fmtDuration, truncate } from './util.js';
|
||||||
import '@hive/shared/hive-tab-strip.js';
|
import '@hive/shared/hive-tab-strip.js';
|
||||||
import { mountJobqGraph } from '@hive/shared/jobq-graph.js';
|
import { JobqGraph } from '@hive/shared/jobq-graph.js';
|
||||||
|
|
||||||
// ─── derived state ───────────────────────────────────────────────────────────
|
// ─── derived state ───────────────────────────────────────────────────────────
|
||||||
let metaInputsState = [];
|
let metaInputsState = [];
|
||||||
|
|
@ -26,7 +27,7 @@ let metaUpdateRunning = false;
|
||||||
// the tree itself; this flat array exists only for the two things it
|
// the tree itself; this flat array exists only for the two things it
|
||||||
// doesn't render: the count-pill and the live-log panel.
|
// doesn't render: the count-pill and the live-log panel.
|
||||||
let jobqNodes = [];
|
let jobqNodes = [];
|
||||||
let jobqGraphHandle = null;
|
let jobqGraphToken = 0;
|
||||||
|
|
||||||
function syncFromSnapshot(s) {
|
function syncFromSnapshot(s) {
|
||||||
metaInputsState = (s.meta_inputs || []).slice();
|
metaInputsState = (s.meta_inputs || []).slice();
|
||||||
|
|
@ -127,27 +128,40 @@ function renderMetaInputs(s) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
||||||
// R3BU1LD QU3U3 is `JobqGraph` (Preact, mounted imperatively — see
|
// R3BU1LD QU3U3 is `JobqGraph` (Preact, rendered via plain `render(h(...))`
|
||||||
// mountJobqGraph's own doc comment in @hive/shared/jobq-graph.js for why
|
// — no JSX pragma needed for either call, no mount wrapper: `render` is
|
||||||
// this file has no JSX pipeline) directly — no hand-rolled tree/roll-up
|
// already the re-render/diff entry point, per mara on review) directly —
|
||||||
// rendering here anymore. The component owns fetching GET /api/jobq/graph
|
// no hand-rolled tree/roll-up rendering here anymore. The component owns
|
||||||
// and its own refetch (`.refresh()` on the mount handle); this page just
|
// fetching GET /api/jobq/graph and its own refetch (bump `jobqGraphToken`
|
||||||
// reads its `onUpdate` callback to keep `jobqNodes` (the flat array) in
|
// and render again); this page just reads its `onUpdate` callback to keep
|
||||||
// sync for the two things the generic view doesn't render itself: the
|
// `jobqNodes` (the flat array) in sync for the two things the generic
|
||||||
// count-pill and the live-log panel below.
|
// view doesn't render itself: the count-pill and the live-log panel below.
|
||||||
//
|
//
|
||||||
// Cancel is the one action this page *does* wire up: the `cancellable`
|
// Cancel is the one action this page *does* wire up: the `cancellable`
|
||||||
// prop turns on the component's own per-node cancel button, which calls
|
// prop turns on the component's own per-node cancel button, which calls
|
||||||
// `onCancel(id)` rather than posting anything — the endpoint
|
// `onCancel(id)` rather than posting anything — the endpoint
|
||||||
// (`/api/rebuild-queue/{id}/cancel`) is this page's domain concept, not
|
// (`/api/rebuild-queue/{id}/cancel`) is this page's domain concept, not
|
||||||
// the generic component's.
|
// the generic component's.
|
||||||
|
// Split from `renderRebuildQueue` below: `replaceChildren()` only
|
||||||
|
// belongs on the *first* render, to clear the static "loading…"
|
||||||
|
// placeholder `#rebuild-queue-section` starts with in `builds.html` —
|
||||||
|
// Preact doesn't know that placeholder is there, so a later render
|
||||||
|
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
|
||||||
|
// tracked children out from under its diffing rather than let it
|
||||||
|
// update them minimally).
|
||||||
function mountRebuildQueue() {
|
function mountRebuildQueue() {
|
||||||
const root = $('rebuild-queue-section');
|
const root = $('rebuild-queue-section');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.replaceChildren();
|
root.replaceChildren();
|
||||||
jobqGraphHandle = mountJobqGraph(root, {
|
renderRebuildQueue();
|
||||||
|
}
|
||||||
|
function renderRebuildQueue() {
|
||||||
|
const root = $('rebuild-queue-section');
|
||||||
|
if (!root) return;
|
||||||
|
render(h(JobqGraph, {
|
||||||
endpoint: '/api/jobq/graph',
|
endpoint: '/api/jobq/graph',
|
||||||
cancellable: true,
|
cancellable: true,
|
||||||
|
refreshToken: jobqGraphToken,
|
||||||
onUpdate: (nodes) => {
|
onUpdate: (nodes) => {
|
||||||
jobqNodes = nodes || [];
|
jobqNodes = nodes || [];
|
||||||
renderRebuildLiveLog();
|
renderRebuildLiveLog();
|
||||||
|
|
@ -165,12 +179,12 @@ function mountRebuildQueue() {
|
||||||
if (!r.ok) throw new Error('http ' + r.status);
|
if (!r.ok) throw new Error('http ' + r.status);
|
||||||
// No manual refresh: cancel flips node state, which fires
|
// No manual refresh: cancel flips node state, which fires
|
||||||
// rebuild_queue_changed over SSE — the existing handler below
|
// rebuild_queue_changed over SSE — the existing handler below
|
||||||
// already calls jobqGraphHandle.refresh() on that tick.
|
// already bumps jobqGraphToken and re-renders on that tick.
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('cancel failed', err);
|
console.error('cancel failed', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
}), root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── running-rebuild live log ─────────────────────────────────────────────────
|
// ─── running-rebuild live log ─────────────────────────────────────────────────
|
||||||
|
|
@ -471,7 +485,8 @@ const SSE_HANDLERS = {
|
||||||
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the
|
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the
|
||||||
// swarm badges, untouched by this), this page just treats it as a
|
// swarm badges, untouched by this), this page just treats it as a
|
||||||
// refetch trigger. `JobqGraph` owns the actual fetch.
|
// refetch trigger. `JobqGraph` owns the actual fetch.
|
||||||
if (jobqGraphHandle) jobqGraphHandle.refresh();
|
jobqGraphToken += 1;
|
||||||
|
renderRebuildQueue();
|
||||||
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
|
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
|
||||||
if (buildTabs && buildTabs.active() === 'buildlogs') {
|
if (buildTabs && buildTabs.active() === 'buildlogs') {
|
||||||
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
referenced from dashboard JS (the schedules view uses `.rqe-source*`),
|
referenced from dashboard JS (the schedules view uses `.rqe-source*`),
|
||||||
so the dashboard pulls them in here. */
|
so the dashboard pulls them in here. */
|
||||||
@import "./system-sections.css";
|
@import "./system-sections.css";
|
||||||
|
@import "@hive/shared/jobq-rollup.css";
|
||||||
|
|
||||||
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
||||||
Top-of-page sticky header with banner + tab strip. SSE stays
|
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. */
|
Notification controls below sit between the banner and the containers. */
|
||||||
|
|
||||||
/* Build-queue summary banner on the SW4RM tab: one compact line
|
/* Build-queue summary banner on the SW4RM tab moved to the shared
|
||||||
when the rebuild queue has active work, with a link to the full queue on
|
`JobqRollup` Preact component (`.jqr-*` classes, imported above) —
|
||||||
the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. */
|
mounted by swarm.js::initJobqRollup into #jobq-rollup-section. */
|
||||||
.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; }
|
|
||||||
|
|
||||||
/* .notif-row / .btn-notif moved to settings.css with the S3TT1NGS
|
/* .notif-row / .btn-notif moved to settings.css with the S3TT1NGS
|
||||||
page. */
|
page. */
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,13 @@
|
||||||
<!-- Swarm / hive identity headline. Populated by refreshState from
|
<!-- Swarm / hive identity headline. Populated by refreshState from
|
||||||
hive_name + swarm_name; stays hidden when neither is set. -->
|
hive_name + swarm_name; stays hidden when neither is set. -->
|
||||||
<h2 id="swarm-identity" hidden></h2>
|
<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">
|
<div id="containers-section">
|
||||||
<p class="meta">loading…</p>
|
<p class="meta">loading…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import {
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
import { el } from '@hive/shared/dom.js';
|
import { el } from '@hive/shared/dom.js';
|
||||||
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
|
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
import { JobqRollup } from '@hive/shared/jobq-rollup.js';
|
||||||
import {
|
import {
|
||||||
containersState, questionsState,
|
containersState, questionsState,
|
||||||
} from './state.js';
|
} from './state.js';
|
||||||
|
|
@ -62,35 +64,39 @@ const selectionState = new Set();
|
||||||
// a card's pending badges are transients-only now, which already means
|
// a card's pending badges are transients-only now, which already means
|
||||||
// "what is running") and this banner, which was pulled entirely per "dont
|
// "what is running") and this banner, which was pulled entirely per "dont
|
||||||
// replace one legacy thing with another" (a client-side tally over the
|
// replace one legacy thing with another" (a client-side tally over the
|
||||||
// generic graph was itself judged a stopgap). Now that the dedicated
|
// generic graph was itself judged a stopgap). Rendering itself later
|
||||||
// rollup endpoint exists (hive-jobq-wire::state_rollup, served at
|
// moved out to the shared `JobqRollup` Preact component (same one
|
||||||
// GET /api/jobq/rollup), the banner reads *that* instead — a handful of
|
// swarm-ui's /jobs page mounts), which owns its own fetch of
|
||||||
// pre-tallied counts, not the graph.
|
// GET /api/jobq/rollup — this file just calls Preact's own
|
||||||
let jobqRollupState = [];
|
// `render(h(...))` directly (no `mountX()` wrapper: `render` is
|
||||||
|
// already the re-render/diff entry point, per mara on review) and
|
||||||
|
// bumps a refresh token to force a refetch.
|
||||||
|
//
|
||||||
|
// Rendered 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) — rendering into a
|
||||||
|
// section that gets wiped on every container-state tick would defeat
|
||||||
|
// the component owning its own fetch lifecycle.
|
||||||
|
let jobqRollupToken = 0;
|
||||||
|
|
||||||
// Fetches the rollup fresh and re-renders. Called on cold load (see
|
export function initJobqRollup() {
|
||||||
// tabs.js's refreshState) and whenever `rebuild_queue_changed` fires
|
const root = $('jobq-rollup-section');
|
||||||
// (applyRebuildQueueChanged below) — a payload-less push trigger by
|
if (!root) return;
|
||||||
|
render(h(JobqRollup, {
|
||||||
|
endpoint: '/api/jobq/rollup',
|
||||||
|
queueHref: '/builds.html',
|
||||||
|
refreshToken: jobqRollupToken,
|
||||||
|
}), root);
|
||||||
|
}
|
||||||
|
// 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
|
// design, confirmed with atlas on the jobq-deletion tracker: the event
|
||||||
// carries no `queue` field this page reads, same "something changed,
|
// 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().
|
// 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() {
|
export function applyRebuildQueueChanged() {
|
||||||
refreshJobqRollup();
|
jobqRollupToken += 1;
|
||||||
|
initJobqRollup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── transients ─────────────────────────────────────────────────────────────
|
// ─── transients ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -627,31 +633,9 @@ export function renderContainers(s) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue-summary banner: one compact line above the container list when
|
// Queue-summary banner lives outside this section now — see
|
||||||
// the job queue has active work, linking to the full queue on the
|
// #jobq-rollup-section / initJobqRollup, mounted once rather than
|
||||||
// BU1LDS page. Reads GET /api/jobq/rollup's pre-tallied `roots` counts
|
// rebuilt on every render this function does.
|
||||||
// (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 →'),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!containers.length && !transientsState.size) {
|
if (!containers.length && !transientsState.size) {
|
||||||
root.append(el('p', { class: 'empty' }, 'no managed containers'));
|
root.append(el('p', { class: 'empty' }, 'no managed containers'));
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import {
|
||||||
renderQuestions, activeQuestionCount,
|
renderQuestions, activeQuestionCount,
|
||||||
} from './call.js';
|
} from './call.js';
|
||||||
import {
|
import {
|
||||||
refreshJobqRollup, syncTransientsFromSnapshot,
|
initJobqRollup, syncTransientsFromSnapshot,
|
||||||
applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved,
|
applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved,
|
||||||
applyTransientSet, applyTransientCleared,
|
applyTransientSet, applyTransientCleared,
|
||||||
renderContainers, renderContainersFromState,
|
renderContainers, renderContainersFromState,
|
||||||
|
|
@ -232,13 +232,12 @@ window.marked = marked;
|
||||||
syncTransientsFromSnapshot(s);
|
syncTransientsFromSnapshot(s);
|
||||||
syncContainersFromSnapshot(s);
|
syncContainersFromSnapshot(s);
|
||||||
// Job-queue rollup feeds only the SW4RM queue-summary banner
|
// Job-queue rollup feeds only the SW4RM queue-summary banner
|
||||||
// (per-agent card badges are transient-only — see swarm.js).
|
// (per-agent card badges are transient-only — see swarm.js). Its
|
||||||
// Self-fetches GET /api/jobq/rollup — not read off `s` (this
|
// own `JobqRollup` mount self-fetches GET /api/jobq/rollup — not
|
||||||
// page's snapshot carries no jobq field) — fire-and-forget:
|
// read off `s` (this page's snapshot carries no jobq field).
|
||||||
// renderContainers below runs off whatever jobqRollupState
|
// initJobqRollup mounts once and is a no-op on later calls; the
|
||||||
// already holds, and refreshJobqRollup's own re-render catches
|
// mount's own effect handles the actual fetch.
|
||||||
// up once the fetch resolves.
|
initJobqRollup();
|
||||||
refreshJobqRollup();
|
|
||||||
renderContainers(s);
|
renderContainers(s);
|
||||||
// Sync the derived approvals + questions stores from the
|
// Sync the derived approvals + questions stores from the
|
||||||
// snapshot, then render. Live `*_added` / `*_resolved` events
|
// snapshot, then render. Live `*_added` / `*_resolved` events
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@
|
||||||
"./hive-warn.js": "./src/hive-warn/hive-warn.js",
|
"./hive-warn.js": "./src/hive-warn/hive-warn.js",
|
||||||
"./side-panel.js": "./src/side-panel/hive-side-panel.js",
|
"./side-panel.js": "./src/side-panel/hive-side-panel.js",
|
||||||
"./jobq-graph.js": "./src/jobq-graph/JobqGraph.tsx",
|
"./jobq-graph.js": "./src/jobq-graph/JobqGraph.tsx",
|
||||||
"./jobq-graph.css": "./src/jobq-graph/jobq-graph.css"
|
"./jobq-graph.css": "./src/jobq-graph/jobq-graph.css",
|
||||||
|
"./jobq-rollup.js": "./src/jobq-rollup/JobqRollup.tsx",
|
||||||
|
"./jobq-rollup.css": "./src/jobq-rollup/jobq-rollup.css"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/"
|
"src/"
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
// needing the raw list (a count badge, a live-log panel) without its
|
// needing the raw list (a count badge, a live-log panel) without its
|
||||||
// own parallel fetch.
|
// own parallel fetch.
|
||||||
//
|
//
|
||||||
// Two ways to use this: JSX (swarm-ui, or any dashboard page that
|
// JSX (swarm-ui) — `<JobqGraph endpoint="..." cancellable
|
||||||
// renders it directly) — `<JobqGraph endpoint="..." cancellable
|
// onCancel={...} onUpdate={...} />`. Or plain `render(h(JobqGraph,
|
||||||
// onCancel={...} onUpdate={...} />`. Or imperative mount
|
// props), container)` (dashboard/src/builds.js, no JSX pragma needed)
|
||||||
// (dashboard/src/builds.js, plain `.js` — mounting needs no JSX pragma)
|
// — call again with a bumped `refreshToken` to refetch.
|
||||||
// — `mountJobqGraph(container, props)` returns `{ refresh(), update(props) }`.
|
|
||||||
//
|
//
|
||||||
// Styles live in `@hive/shared/jobq-graph.css`, `@import`ed from a
|
// Styles live in `@hive/shared/jobq-graph.css`, `@import`ed from a
|
||||||
// page/component CSS file rather than imported here — dashboard bundles
|
// page/component CSS file rather than imported here — dashboard bundles
|
||||||
|
|
@ -29,7 +28,6 @@
|
||||||
// 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 { h, render } from 'preact';
|
|
||||||
import { useState, useEffect, useCallback } from 'preact/hooks';
|
import { useState, useEffect, useCallback } from 'preact/hooks';
|
||||||
|
|
||||||
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
|
// Mirrors `hive_jobq_wire::StateSchema` verbatim (variant names, no
|
||||||
|
|
@ -233,8 +231,8 @@ export interface JobqGraphProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// `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
|
||||||
// change identity so the effect below re-runs, giving a host (or
|
// change identity so the effect below re-runs, giving a host an
|
||||||
// `mountJobqGraph`) an explicit "refetch now" lever without an
|
// explicit "refetch now" lever (bump it and re-render) without an
|
||||||
// imperative ref into this component.
|
// imperative ref into this component.
|
||||||
export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, refreshToken = 0 }: JobqGraphProps) {
|
export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, refreshToken = 0 }: JobqGraphProps) {
|
||||||
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
|
const [selectedStates, setSelectedStates] = useState<Set<NodeState>>(
|
||||||
|
|
@ -294,19 +292,3 @@ export function JobqGraph({ endpoint, cancellable = false, onUpdate, onCancel, r
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imperative mount helper for a call site that isn't itself a JSX file
|
|
||||||
// (dashboard/src/builds.js — plain `.js`, no per-file JSX pragma to
|
|
||||||
// write `<JobqGraph .../>` inline). Returns a handle: `.refresh()`
|
|
||||||
// (re-fetch with the current props) and `.update(props)` (merge new
|
|
||||||
// props — e.g. a different `endpoint` — and re-render).
|
|
||||||
export function mountJobqGraph(container: Element, initialProps: JobqGraphProps) {
|
|
||||||
let props = initialProps;
|
|
||||||
let token = 0;
|
|
||||||
const draw = () => render(h(JobqGraph, { ...props, refreshToken: token }), container);
|
|
||||||
draw();
|
|
||||||
return {
|
|
||||||
refresh() { token += 1; draw(); },
|
|
||||||
update(next: Partial<JobqGraphProps>) { props = { ...props, ...next }; draw(); },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
90
frontend/packages/shared/src/jobq-rollup/JobqRollup.tsx
Normal file
90
frontend/packages/shared/src/jobq-rollup/JobqRollup.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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; }
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/* JobsPage — wraps the shared JobqGraph component pointed at the
|
/* JobsPage — wraps the shared JobqGraph + JobqRollup components pointed
|
||||||
swarm-controller's own /api/jobq/graph endpoint (same wire shape
|
at the swarm-controller's own /api/jobq/{graph,rollup} endpoints (same
|
||||||
hive-c0re's dashboard consumes, see hive-jobq-wire's README). Styles
|
wire shapes hive-c0re's dashboard consumes, see hive-jobq-wire's
|
||||||
are @hive/shared's jobq-graph.css, @import'ed here rather than from
|
README). Styles are @hive/shared's own CSS files, @import'ed here
|
||||||
the component file itself — see JobqGraph.tsx's own comment for why
|
rather than from the component files themselves — see JobqGraph.tsx's
|
||||||
that split exists (esbuild's loader map is global per bundle call). */
|
own comment for why that split exists (esbuild's loader map is global
|
||||||
|
per bundle call). */
|
||||||
@import "@hive/shared/jobq-graph.css";
|
@import "@hive/shared/jobq-graph.css";
|
||||||
|
@import "@hive/shared/jobq-rollup.css";
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,19 @@
|
||||||
// SwarmNodeKind/SwarmResourceKind), so this renders an empty tree today
|
// SwarmNodeKind/SwarmResourceKind), so this renders an empty tree today
|
||||||
// — the page exists so the wiring is in place before the first real
|
// — the page exists so the wiring is in place before the first real
|
||||||
// swarm-level job (e.g. CreateAgent) lands.
|
// swarm-level job (e.g. CreateAgent) lands.
|
||||||
|
//
|
||||||
|
// JobqRollup sits above the graph, same "N running / M queued" banner
|
||||||
|
// the dashboard's SW4RM tab shows — no `queueHref`, since a "view
|
||||||
|
// queue →" link back to this same page would be noise.
|
||||||
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 { Panel } from '../ui/panel/Panel.js';
|
import { Panel } from '../ui/panel/Panel.js';
|
||||||
import './JobsPage.css';
|
import './JobsPage.css';
|
||||||
|
|
||||||
export function JobsPage() {
|
export function JobsPage() {
|
||||||
return (
|
return (
|
||||||
<Panel title="jobs">
|
<Panel title="jobs">
|
||||||
|
<JobqRollup endpoint="/api/jobq/rollup" />
|
||||||
<JobqGraph endpoint="/api/jobq/graph" />
|
<JobqGraph endpoint="/api/jobq/graph" />
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue