From ffb2d78a563f11d2042ad8272ed4b5f35c4799f9 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 25 Jul 2026 21:21:46 +0200 Subject: [PATCH] fix(dashboard): draw jobq tree connectors with CSS lines instead of box-drawing chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Unicode prefix string approach (└─ / ├─ / │ built up as text in a single ) with positioned DOM elements that draw real lines: - rqe-tree-guide: fixed-width ancestor column, optionally draws a full vertical border-left when the ancestor has siblings below it (.rqe-tree-guide-line). - rqe-tree-connector: draws the L/T shape via ::before (vertical stem, top→center for last child, full height for mid child) and ::after (horizontal spur, center→right). .rqe-tree-connector-last vs .rqe-tree-connector-mid controls stem length. renderTreeNode() now takes ancestorLines: boolean[] instead of a prefix string. Each entry is true when the ancestor at that depth was not the last child (so a vertical guide is still needed through that column). childAncestorLines propagates depth === 0 correctly (root nodes have no guide columns, so their children start with an empty array). Lines are drawn with var(--border) so they follow the theme and work at any font size without alignment drift. Addresses the review note on PR 2686. --- frontend/packages/dashboard/src/builds.js | 29 +++++++--- .../dashboard/src/system-sections.css | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js index 0e8cce86..26d24f97 100644 --- a/frontend/packages/dashboard/src/builds.js +++ b/frontend/packages/dashboard/src/builds.js @@ -325,13 +325,24 @@ function renderQueueEntry(entry) { if (nodes.length) { const treeRoot = el('div', { class: 'rqe-nodes-tree' }); const treeNodes = buildNodeTree(nodes); - function renderTreeNode(n, depth, isLast, prefix) { + function renderTreeNode(n, depth, isLast, ancestorLines) { + // ancestorLines: boolean[] where true = draw a vertical guide line at + // that ancestor depth level (the ancestor was not the last sibling, so + // its remaining siblings need a guide column below it). const row = el('div', { class: 'rqe-tree-row' }); if (depth > 0) { - // prefix: the inherited connector string for ancestor columns - // isLast: whether this node is the last sibling (use └ vs ├) - row.append(el('span', { class: 'rqe-tree-indent' }, - prefix + (isLast ? '└─ ' : '├─ '))); + // One guide column per ancestor level — draws a vertical line through + // columns where the ancestor still has siblings below it. + for (const hasLine of ancestorLines) { + row.append(el('span', { + class: 'rqe-tree-guide' + (hasLine ? ' rqe-tree-guide-line' : ''), + })); + } + // Connector: L-shaped for last child, T-shaped for mid child. + row.append(el('span', { + class: 'rqe-tree-connector' + + (isLast ? ' rqe-tree-connector-last' : ' rqe-tree-connector-mid'), + })); } const chip = el('span', { class: 'rqe-node rqe-node-' + n.state, @@ -351,12 +362,14 @@ function renderQueueEntry(entry) { }, '⎙')); } treeRoot.append(row); - const childPrefix = depth > 0 ? prefix + (isLast ? ' ' : '│ ') : ''; + // Propagate ancestor lines to children: inherit this node's columns, + // plus whether this node itself continues below (not the last sibling). + const childAncestorLines = depth === 0 ? [] : [...ancestorLines, !isLast]; n._children.forEach((child, i) => { - renderTreeNode(child, depth + 1, i === n._children.length - 1, childPrefix); + renderTreeNode(child, depth + 1, i === n._children.length - 1, childAncestorLines); }); } - treeNodes.forEach((n, i) => renderTreeNode(n, 0, i === treeNodes.length - 1, '')); + treeNodes.forEach((n, i) => renderTreeNode(n, 0, i === treeNodes.length - 1, [])); li.append(treeRoot); } const failed = firstFailedNode(entry); diff --git a/frontend/packages/dashboard/src/system-sections.css b/frontend/packages/dashboard/src/system-sections.css index c712ed1f..07aa6338 100644 --- a/frontend/packages/dashboard/src/system-sections.css +++ b/frontend/packages/dashboard/src/system-sections.css @@ -142,6 +142,59 @@ 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);