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
|
|
@ -53,6 +53,30 @@
|
|||
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 {
|
||||
margin: 0.1em 0 0 1.6em;
|
||||
font-size: 0.85em;
|
||||
|
|
|
|||
|
|
@ -10,19 +10,19 @@
|
|||
// a build-log link, ...) does its own thing on top; this is the generic
|
||||
// floor every jobq gets for free.
|
||||
//
|
||||
// `Node`-kind dep edges get a plain "waits on: <label>" line under the
|
||||
// dependent node — see `buildTree`'s `_waitsOn` resolution below. (A
|
||||
// gutter rail was tried first; dropped since it spans by sibling-array
|
||||
// position, and a nested subtree between two siblings breaks that into
|
||||
// disconnected ticks. Text has no such gap.)
|
||||
// `Node`-kind dep edges get a plain "waits on: <label>" text line — see
|
||||
// `buildTree`'s `_waitsOn` resolution. `cancellable` attribute adds a
|
||||
// per-node cancel button dispatching `hive-jobq-graph-cancel` instead of
|
||||
// POSTing anything itself — see `CANCELLABLE_STATES` + the click-delegate
|
||||
// in `connectedCallback` below for both.
|
||||
//
|
||||
// Usage: <hive-jobq-graph endpoint="/api/jobq/graph"></hive-jobq-graph> —
|
||||
// self-fetches on connect. `.refresh()` (public) re-fetches + re-renders;
|
||||
// `.render(nodes)` (public) renders host-pushed data directly, no fetch.
|
||||
// Every render dispatches a bubbling/composed `hive-jobq-graph-update`
|
||||
// event (`detail: { nodes }`) so a host needing the raw list for
|
||||
// something the tree doesn't show (a count badge, a live-log panel)
|
||||
// listens instead of running its own parallel fetch.
|
||||
// self-fetches on connect. `.refresh()` re-fetches + re-renders;
|
||||
// `.render(nodes)` renders host-pushed data directly, no fetch. Every
|
||||
// render dispatches a bubbling/composed `hive-jobq-graph-update` event
|
||||
// (`detail: { nodes }`) so a host needing the raw list for something the
|
||||
// tree doesn't show (a count badge, a live-log panel) listens instead of
|
||||
// running its own parallel fetch.
|
||||
//
|
||||
// Shadow DOM + own styles, per instruction — unlike light-DOM
|
||||
// <hive-tab-strip>, this renders a whole subtree nothing else needs to
|
||||
|
|
@ -43,6 +43,11 @@ const STATE_GLYPH = {
|
|||
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
|
||||
// grouping) defines tree shape. Sibling order follows array order, which is
|
||||
// already root-then-subtree per root per `GraphWire::wire_snapshot`'s own
|
||||
|
|
@ -94,8 +99,9 @@ function renderDataList(data) {
|
|||
return dl;
|
||||
}
|
||||
|
||||
function renderNode(n) {
|
||||
function renderNode(n, cancellable) {
|
||||
const glyph = STATE_GLYPH[n.state] || '?';
|
||||
const showCancel = cancellable && CANCELLABLE_STATES.has(n.state);
|
||||
const row = el('div', { class: 'jg-row' },
|
||||
el('span', {
|
||||
class: 'jg-state jg-state-' + n.state.toLowerCase(),
|
||||
|
|
@ -103,6 +109,12 @@ function renderNode(n) {
|
|||
}, glyph),
|
||||
' ',
|
||||
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);
|
||||
if (n._waitsOn && n._waitsOn.length) {
|
||||
|
|
@ -111,7 +123,7 @@ function renderNode(n) {
|
|||
const data = renderDataList(n.payload.data);
|
||||
if (data) wrap.append(data);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +136,18 @@ class HiveJobqGraph extends HTMLElement {
|
|||
this._root = attachShadowCss(this, graphCss);
|
||||
this._body = el('div', { class: 'jg-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();
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +178,8 @@ class HiveJobqGraph extends HTMLElement {
|
|||
this._body.append(el('p', { class: 'jg-empty' }, 'empty'));
|
||||
} else {
|
||||
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', {
|
||||
detail: { nodes: nodes || [] },
|
||||
|
|
|
|||
Loading…
Reference in a new issue