jobq-graph: re-add per-node cancel button
Fixes #3067. <hive-jobq-graph> gains a `cancellable` attribute: any non-terminal node (Pending/Running/Finishing) gets a small cancel button, and a click dispatches `hive-jobq-graph-cancel` (`detail: { id }`) rather than POSTing anything itself -- which endpoint actually cancels a node is the host's domain concept, same "push data out, host decides" shape `hive-jobq-graph-update` already uses. builds.js turns it on for R3BU1LD QU3U3, confirms via themedConfirm, then POSTs the existing `/api/rebuild-queue/{id}/cancel` endpoint. No manual refresh needed -- cancelling flips node state, which already fires rebuild_queue_changed over SSE, and the page's existing handler for that tick already calls jobqGraphEl.refresh(). Also removed ~130 lines of dead `.rqe-*` CSS in system-sections.css left over from the bespoke pre-<hive-jobq-graph> queue renderer (confirmed zero JS references before deleting each rule; kept the still-used `.rqe-kind`/`.rqe-agent`/`.rqe-source*`). docs/web-ui/dashboard.md's R3BU1LD QU3U3 section updated to match current behaviour (cancel button, waits-on text instead of the old "no per-node actions" note, sibling order no longer implies anything since deps render as text not a reordered rail).
This commit is contained in:
parent
a858739b28
commit
aa14339be7
5 changed files with 117 additions and 176 deletions
|
|
@ -232,21 +232,28 @@ dashboard pages treat the tick as a pure refetch trigger).
|
||||||
|
|
||||||
Each row is one root graph node (`parent: null`); a multi-step op's
|
Each row is one root graph node (`parent: null`); a multi-step op's
|
||||||
per-agent subgraphs and sub-steps render as nodes within that one
|
per-agent subgraphs and sub-steps render as nodes within that one
|
||||||
entry (structural `parent` edges define the tree, `deps` edges order
|
entry (structural `parent` edges define the tree; sibling order is
|
||||||
siblings). A row shows a state glyph (`⏸` pending / `▶` running /
|
array order, unchanged from the wire). A row shows a state glyph (`⏸`
|
||||||
`◐` finishing — own work done, a sub-node still running / `✔` done /
|
pending / `▶` running / `◐` finishing — own work done, a sub-node
|
||||||
`✖` failed / `⊘` cancelled / `·` skipped) and each step's own
|
still running / `✔` done / `✖` failed / `⊘` cancelled / `·` skipped)
|
||||||
label/agent. **No source chip, kind label, cancel button, timing, or
|
and each step's own label/agent. A `Node`-kind dep on a sibling shows
|
||||||
build-log deep-link on rows** — the generic component has no
|
as a plain "waits on: `<label>`" text line under the row — an earlier
|
||||||
per-node action affordances or entry-level metadata (no equivalent of
|
gutter-rail version was reverted (it broke visually whenever a nested
|
||||||
the old `DagView`'s `source`/`reason`/`created_at`, which were
|
subtree sat between the two related rows; text has no such gap).
|
||||||
`NodeKind::Dag`-specific fields the generic wire doesn't carry); per
|
`builds.js` mounts the element with `cancellable` set, which turns on
|
||||||
mara's steer on hyperhive#2812 ("dont feel constrained by what the ui
|
a per-node cancel button (`✕`) on any non-terminal row — the button
|
||||||
does currently"), the first cut presents what the endpoint actually
|
dispatches `hive-jobq-graph-cancel`, and the page does the actual
|
||||||
gives rather than reconstructing the old per-row chrome. Settled
|
`POST /api/rebuild-queue/{id}/cancel`, matching the wire event as its
|
||||||
entries render their **full step tree**, not just a bare summary —
|
own domain concept (the component knows nothing about that endpoint).
|
||||||
unlike the old `DagView` projection, this wire does not filter `Done`
|
**Still no source chip, kind label, timing, or build-log deep-link on
|
||||||
nodes out.
|
rows** — no equivalent of the old `DagView`'s `source`/`reason`/
|
||||||
|
`created_at`, which were `NodeKind::Dag`-specific fields the generic
|
||||||
|
wire doesn't carry; per mara's original steer ("dont feel constrained
|
||||||
|
by what the ui does currently"), rows present what the endpoint
|
||||||
|
actually gives rather than reconstructing the old per-row chrome.
|
||||||
|
Settled entries render their **full step tree**, not just a bare
|
||||||
|
summary — unlike the old `DagView` projection, this wire does not
|
||||||
|
filter `Done` nodes out.
|
||||||
|
|
||||||
Below the queue, a **live build-log panel** (`#rebuild-live-log`,
|
Below the queue, a **live build-log panel** (`#rebuild-live-log`,
|
||||||
`renderRebuildLiveLog`) shows the currently-running rebuild's output
|
`renderRebuildLiveLog`) shows the currently-running rebuild's output
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import { $, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings } from './common.js';
|
import { $, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings } from './common.js';
|
||||||
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 { 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 '@hive/shared/jobq-graph.js';
|
import '@hive/shared/jobq-graph.js';
|
||||||
|
|
@ -128,25 +129,46 @@ function renderMetaInputs(s) {
|
||||||
|
|
||||||
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
||||||
// R3BU1LD QU3U3 is <hive-jobq-graph> directly (mara: "replace the build
|
// R3BU1LD QU3U3 is <hive-jobq-graph> directly (mara: "replace the build
|
||||||
// queue tab with this component") — no hand-rolled
|
// queue tab with this component") — no hand-rolled tree/roll-up rendering
|
||||||
// tree/roll-up/cancel-button rendering here anymore. The component owns
|
// here anymore. The component owns fetching GET /api/jobq/graph and its
|
||||||
// fetching GET /api/jobq/graph and its own refresh(); this page just
|
// own refresh(); this page just listens for its `hive-jobq-graph-update`
|
||||||
// listens for its `hive-jobq-graph-update` event to keep `jobqNodes` (the
|
// event to keep `jobqNodes` (the flat array) in sync for the two things
|
||||||
// flat array) in sync for the two things the generic view doesn't render:
|
// the generic view doesn't render itself: the count-pill and the live-log
|
||||||
// the count-pill and the live-log panel below. No cancel button or
|
// panel below.
|
||||||
// build-log deep-link on rows either — the generic component has no
|
//
|
||||||
// per-node action affordances; per "dont feel constrained by what the ui
|
// Cancel is the one action this page *does* wire up: the
|
||||||
// does currently," not reinventing those here for the first cut.
|
// `cancellable` attribute turns on the component's own per-node cancel
|
||||||
|
// button, which dispatches `hive-jobq-graph-cancel` rather than posting
|
||||||
|
// anything — the endpoint (`/api/rebuild-queue/{id}/cancel`) is this
|
||||||
|
// page's domain concept, not the generic component's.
|
||||||
function mountJobqGraph() {
|
function mountJobqGraph() {
|
||||||
const root = $('rebuild-queue-section');
|
const root = $('rebuild-queue-section');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.replaceChildren();
|
root.replaceChildren();
|
||||||
jobqGraphEl = el('hive-jobq-graph', { endpoint: '/api/jobq/graph' });
|
jobqGraphEl = el('hive-jobq-graph', { endpoint: '/api/jobq/graph', cancellable: '' });
|
||||||
jobqGraphEl.addEventListener('hive-jobq-graph-update', (e) => {
|
jobqGraphEl.addEventListener('hive-jobq-graph-update', (e) => {
|
||||||
jobqNodes = e.detail.nodes || [];
|
jobqNodes = e.detail.nodes || [];
|
||||||
renderRebuildLiveLog();
|
renderRebuildLiveLog();
|
||||||
updateRebuildCount();
|
updateRebuildCount();
|
||||||
});
|
});
|
||||||
|
jobqGraphEl.addEventListener('hive-jobq-graph-cancel', async (e) => {
|
||||||
|
const { id } = e.detail;
|
||||||
|
const node = jobqNodes.find((n) => n.id === id);
|
||||||
|
const label = node ? node.payload.label : 'node ' + id;
|
||||||
|
if (!(await themedConfirm({
|
||||||
|
message: `cancel ${label}? a group root cancels the whole subtree; a mid-tree node cancels just that branch.`,
|
||||||
|
danger: true, confirmLabel: '✕ cancel',
|
||||||
|
}))) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/rebuild-queue/' + id + '/cancel', { method: 'POST' });
|
||||||
|
if (!r.ok) throw new Error('http ' + r.status);
|
||||||
|
// No manual refresh: cancel flips node state, which fires
|
||||||
|
// rebuild_queue_changed over SSE — the existing handler below
|
||||||
|
// already calls jobqGraphEl.refresh() on that tick.
|
||||||
|
} catch (err) {
|
||||||
|
console.error('cancel failed', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
root.append(jobqGraphEl);
|
root.append(jobqGraphEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,15 +105,6 @@
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.4em;
|
gap: 0.4em;
|
||||||
}
|
}
|
||||||
.rebuild-queue-entry.rqe-running {
|
|
||||||
border-color: var(--purple);
|
|
||||||
background: color-mix(in srgb, var(--purple) 12%, transparent);
|
|
||||||
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.rebuild-queue-entry.rqe-failed { border-color: var(--red); color: var(--red); }
|
|
||||||
.rebuild-queue-entry.rqe-cancelled { opacity: 0.6; }
|
|
||||||
.rebuild-queue-entry.rqe-done { opacity: 0.7; color: var(--green); }
|
|
||||||
.rqe-state { font-weight: bold; min-width: 1.2em; text-align: center; }
|
|
||||||
.rqe-kind { color: var(--cyan); }
|
.rqe-kind { color: var(--cyan); }
|
||||||
.rqe-agent { color: var(--amber); font-weight: bold; }
|
.rqe-agent { color: var(--amber); font-weight: bold; }
|
||||||
.rqe-source {
|
.rqe-source {
|
||||||
|
|
@ -130,134 +121,6 @@
|
||||||
.rqe-source-auto_update { color: var(--muted); }
|
.rqe-source-auto_update { color: var(--muted); }
|
||||||
.rqe-source-crash_recover { color: var(--amber); border-color: var(--amber); }
|
.rqe-source-crash_recover { color: var(--amber); border-color: var(--amber); }
|
||||||
.rqe-source-approval { color: var(--green); border-color: var(--green); }
|
.rqe-source-approval { color: var(--green); border-color: var(--green); }
|
||||||
.rqe-when { color: var(--muted); font-size: 0.85em; }
|
|
||||||
.rqe-reason { color: var(--muted); font-size: 0.85em; flex: 1 1 auto; }
|
|
||||||
/* Per-node DAG chain: one chip per primitive node, dependency order. */
|
|
||||||
.rqe-nodes {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin: 0.15em 0 0 1.8em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.15em;
|
|
||||||
}
|
|
||||||
/* ─── jobq node tree (replaces .rqe-nodes for tree-structured payloads) ──── */
|
|
||||||
.rqe-nodes-tree {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin: 0.25em 0 0 1.8em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
.rqe-tree-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
min-height: 1.6em;
|
|
||||||
}
|
|
||||||
/* Guide column: fixed-width spacer that optionally draws a vertical guide
|
|
||||||
line through rows where a sibling of an ancestor continues below. */
|
|
||||||
.rqe-tree-guide,
|
|
||||||
.rqe-tree-connector {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 1.1em;
|
|
||||||
align-self: stretch;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.rqe-tree-guide-line::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
/* Connector: vertical stem from top, horizontal spur to the right.
|
|
||||||
Last child (└): stem goes top→center. Mid child (├): stem is full height. */
|
|
||||||
.rqe-tree-connector::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 0;
|
|
||||||
bottom: 50%;
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.rqe-tree-connector-mid::before {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
.rqe-tree-connector::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
right: 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.rqe-node {
|
|
||||||
padding: 0.05em 0.45em;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 0.7em;
|
|
||||||
color: var(--muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.rqe-node-running {
|
|
||||||
color: var(--purple);
|
|
||||||
border-color: var(--purple);
|
|
||||||
animation: badge-pulse 1.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.rqe-node-done { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); }
|
|
||||||
.rqe-node-failed { color: var(--red); border-color: var(--red); }
|
|
||||||
.rqe-node-cancelled { opacity: 0.55; text-decoration: line-through; }
|
|
||||||
/* Not-taken outcome branch — expected, quiet, distinct from cancelled
|
|
||||||
(dimmed only, no strikethrough: this wasn't dropped mid-flight, it was
|
|
||||||
never going to run). */
|
|
||||||
.rqe-node-skipped { opacity: 0.5; }
|
|
||||||
.rqe-node-arrow { color: var(--muted); }
|
|
||||||
.rqe-node-log { margin-left: 0.1em; text-decoration: none; }
|
|
||||||
|
|
||||||
.rqe-error {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin: 0.3em 0 0;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
background: color-mix(in srgb, var(--red) 10%, transparent);
|
|
||||||
border-left: 2px solid var(--red);
|
|
||||||
color: var(--red);
|
|
||||||
font-size: 0.8em;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.rqe-cancel {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.rqe-cancel-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--purple-dim);
|
|
||||||
color: var(--muted);
|
|
||||||
border-radius: 999px;
|
|
||||||
width: 1.6em;
|
|
||||||
height: 1.6em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
.rqe-cancel-btn:hover:not(:disabled),
|
|
||||||
.rqe-cancel-btn:focus-visible {
|
|
||||||
color: var(--red);
|
|
||||||
border-color: var(--red);
|
|
||||||
box-shadow: 0 0 8px -2px var(--red);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.rqe-cancel-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── running-rebuild live log (renderRebuildLiveLog in builds.js) ──────────
|
/* ─── running-rebuild live log (renderRebuildLiveLog in builds.js) ──────────
|
||||||
Streams the currently-running rebuild's build log inline under the queue.
|
Streams the currently-running rebuild's build log inline under the queue.
|
||||||
Hidden (native `hidden` attr) when nothing is building. Used on
|
Hidden (native `hidden` attr) when nothing is building. Used on
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,30 @@
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jg-cancel-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--muted);
|
||||||
|
color: var(--muted);
|
||||||
|
border-radius: 999px;
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.jg-cancel-btn:hover,
|
||||||
|
.jg-cancel-btn:focus-visible {
|
||||||
|
color: var(--red);
|
||||||
|
border-color: var(--red);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.jg-data {
|
.jg-data {
|
||||||
margin: 0.1em 0 0 1.6em;
|
margin: 0.1em 0 0 1.6em;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,19 @@
|
||||||
// a build-log link, ...) does its own thing on top; this is the generic
|
// a build-log link, ...) does its own thing on top; this is the generic
|
||||||
// floor every jobq gets for free.
|
// floor every jobq gets for free.
|
||||||
//
|
//
|
||||||
// `Node`-kind dep edges get a plain "waits on: <label>" line under the
|
// `Node`-kind dep edges get a plain "waits on: <label>" text line — see
|
||||||
// dependent node — see `buildTree`'s `_waitsOn` resolution below. (A
|
// `buildTree`'s `_waitsOn` resolution. `cancellable` attribute adds a
|
||||||
// gutter rail was tried first; dropped since it spans by sibling-array
|
// per-node cancel button dispatching `hive-jobq-graph-cancel` instead of
|
||||||
// position, and a nested subtree between two siblings breaks that into
|
// POSTing anything itself — see `CANCELLABLE_STATES` + the click-delegate
|
||||||
// disconnected ticks. Text has no such gap.)
|
// in `connectedCallback` below for both.
|
||||||
//
|
//
|
||||||
// Usage: <hive-jobq-graph endpoint="/api/jobq/graph"></hive-jobq-graph> —
|
// Usage: <hive-jobq-graph endpoint="/api/jobq/graph"></hive-jobq-graph> —
|
||||||
// self-fetches on connect. `.refresh()` (public) re-fetches + re-renders;
|
// self-fetches on connect. `.refresh()` re-fetches + re-renders;
|
||||||
// `.render(nodes)` (public) renders host-pushed data directly, no fetch.
|
// `.render(nodes)` renders host-pushed data directly, no fetch. Every
|
||||||
// Every render dispatches a bubbling/composed `hive-jobq-graph-update`
|
// render dispatches a bubbling/composed `hive-jobq-graph-update` event
|
||||||
// event (`detail: { nodes }`) so a host needing the raw list for
|
// (`detail: { nodes }`) so a host needing the raw list for something the
|
||||||
// something the tree doesn't show (a count badge, a live-log panel)
|
// tree doesn't show (a count badge, a live-log panel) listens instead of
|
||||||
// listens instead of running its own parallel fetch.
|
// running its own parallel fetch.
|
||||||
//
|
//
|
||||||
// Shadow DOM + own styles, per instruction — unlike light-DOM
|
// Shadow DOM + own styles, per instruction — unlike light-DOM
|
||||||
// <hive-tab-strip>, this renders a whole subtree nothing else needs to
|
// <hive-tab-strip>, this renders a whole subtree nothing else needs to
|
||||||
|
|
@ -43,6 +43,11 @@ const STATE_GLYPH = {
|
||||||
Skipped: '·',
|
Skipped: '·',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Non-terminal states a cancel button makes sense on. Finishing is
|
||||||
|
// included — "own logic done, children still running" is still a subtree
|
||||||
|
// worth stopping early.
|
||||||
|
const CANCELLABLE_STATES = new Set(['Pending', 'Running', 'Finishing']);
|
||||||
|
|
||||||
// Build a parent/child tree from the flat wire array. `parent` (structural
|
// Build a parent/child tree from the flat wire array. `parent` (structural
|
||||||
// grouping) defines tree shape. Sibling order follows array order, which is
|
// grouping) defines tree shape. Sibling order follows array order, which is
|
||||||
// already root-then-subtree per root per `GraphWire::wire_snapshot`'s own
|
// already root-then-subtree per root per `GraphWire::wire_snapshot`'s own
|
||||||
|
|
@ -94,8 +99,9 @@ function renderDataList(data) {
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNode(n) {
|
function renderNode(n, cancellable) {
|
||||||
const glyph = STATE_GLYPH[n.state] || '?';
|
const glyph = STATE_GLYPH[n.state] || '?';
|
||||||
|
const showCancel = cancellable && CANCELLABLE_STATES.has(n.state);
|
||||||
const row = el('div', { class: 'jg-row' },
|
const row = el('div', { class: 'jg-row' },
|
||||||
el('span', {
|
el('span', {
|
||||||
class: 'jg-state jg-state-' + n.state.toLowerCase(),
|
class: 'jg-state jg-state-' + n.state.toLowerCase(),
|
||||||
|
|
@ -103,6 +109,12 @@ function renderNode(n) {
|
||||||
}, glyph),
|
}, glyph),
|
||||||
' ',
|
' ',
|
||||||
el('span', { class: 'jg-label' }, n.payload.label),
|
el('span', { class: 'jg-label' }, n.payload.label),
|
||||||
|
showCancel
|
||||||
|
? el('button', {
|
||||||
|
type: 'button', class: 'jg-cancel-btn', 'data-cancel-id': String(n.id),
|
||||||
|
title: 'cancel ' + n.payload.label,
|
||||||
|
}, '✕')
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
const wrap = el('div', { class: 'jg-node' }, row);
|
const wrap = el('div', { class: 'jg-node' }, row);
|
||||||
if (n._waitsOn && n._waitsOn.length) {
|
if (n._waitsOn && n._waitsOn.length) {
|
||||||
|
|
@ -111,7 +123,7 @@ function renderNode(n) {
|
||||||
const data = renderDataList(n.payload.data);
|
const data = renderDataList(n.payload.data);
|
||||||
if (data) wrap.append(data);
|
if (data) wrap.append(data);
|
||||||
if (n.error) wrap.append(el('pre', { class: 'jg-error' }, n.error));
|
if (n.error) wrap.append(el('pre', { class: 'jg-error' }, n.error));
|
||||||
for (const child of n._children) wrap.append(renderNode(child));
|
for (const child of n._children) wrap.append(renderNode(child, cancellable));
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +136,18 @@ class HiveJobqGraph extends HTMLElement {
|
||||||
this._root = attachShadowCss(this, graphCss);
|
this._root = attachShadowCss(this, graphCss);
|
||||||
this._body = el('div', { class: 'jg-body' });
|
this._body = el('div', { class: 'jg-body' });
|
||||||
this._root.append(this._body);
|
this._root.append(this._body);
|
||||||
|
// One delegated listener rather than a per-button one — cancel buttons
|
||||||
|
// come and go on every re-render, a delegated listener on the stable
|
||||||
|
// container doesn't need rebinding.
|
||||||
|
this._body.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.jg-cancel-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
this.dispatchEvent(new CustomEvent('hive-jobq-graph-cancel', {
|
||||||
|
detail: { id: Number(btn.dataset.cancelId) },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +178,8 @@ class HiveJobqGraph extends HTMLElement {
|
||||||
this._body.append(el('p', { class: 'jg-empty' }, 'empty'));
|
this._body.append(el('p', { class: 'jg-empty' }, 'empty'));
|
||||||
} else {
|
} else {
|
||||||
const roots = buildTree(nodes);
|
const roots = buildTree(nodes);
|
||||||
for (const root of roots) this._body.append(renderNode(root));
|
const cancellable = this.hasAttribute('cancellable');
|
||||||
|
for (const root of roots) this._body.append(renderNode(root, cancellable));
|
||||||
}
|
}
|
||||||
this.dispatchEvent(new CustomEvent('hive-jobq-graph-update', {
|
this.dispatchEvent(new CustomEvent('hive-jobq-graph-update', {
|
||||||
detail: { nodes: nodes || [] },
|
detail: { nodes: nodes || [] },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue