diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 9990aa9b..028011c5 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -976,11 +976,14 @@ identically.) ### Topology tree -Nothing nests. `swarm.js::buildAgentTree` returns every container at -depth 0 in alphabetical order, and the row builder emits no prefix -column for a depth-0 row — so a container row carries no indent, no -joint and no continuation bar, and the SW4RM list reads as a flat -roster. The `.tree-prefix` lane rules in `dashboard.css` match nothing. +Nothing nests. `swarm.js::sortedContainerRows` returns every container +in alphabetical order, full stop — the SW4RM list is a flat roster, no +indent, no joint, no continuation bar. This used to be a real forest +walk (`buildAgentTree`, keyed on each container's `parent` field) with +a DOM-painted `.tree-prefix` indent ladder. Once the backend dropped +the `parent` field entirely, every container became a root and the +tree machinery could only ever produce this same flat order — so it +was removed rather than kept as code that draws a tree with one level. ## Selection bar diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 71106782..e87d1af7 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -200,84 +200,6 @@ body.dashboard-shell { opacity 200ms ease, border-color 200ms ease; } -/* Topology indent ladder. See docs/web-ui/dashboard.md::Topology tree (Indent - + lane geometry paragraph) for the 1.8em-per-depth-level - rationale + CSS-attr()-not-yet-portable caveat. */ -.container-row[data-depth] { - position: relative; -} -.container-row[data-depth="1"] { - margin-left: 1.8em; -} -.container-row[data-depth="2"] { - margin-left: 3.6em; -} -.container-row[data-depth="3"] { - margin-left: 5.4em; -} -.container-row[data-depth="4"] { - margin-left: 7.2em; -} -.container-row[data-depth="5"] { - margin-left: 9em; -} -.container-row[data-depth="6"] { - margin-left: 10.8em; -} -/* Tree prefix lanes — DOM-painted, not text-glyph-painted. */ -.container-row .tree-prefix { - position: absolute; - top: 0; - bottom: -0.4em; - display: flex; - flex-direction: row; - pointer-events: none; - user-select: none; - color: var(--purple-dim); -} -.container-row[data-depth="1"] .tree-prefix { - left: -1.8em; -} -.container-row[data-depth="2"] .tree-prefix { - left: -3.6em; -} -.container-row[data-depth="3"] .tree-prefix { - left: -5.4em; -} -.container-row[data-depth="4"] .tree-prefix { - left: -7.2em; -} -.container-row[data-depth="5"] .tree-prefix { - left: -9em; -} -.container-row[data-depth="6"] .tree-prefix { - left: -10.8em; -} -.tree-prefix .tree-lane { - flex: 0 0 1.8em; - position: relative; -} -.tree-prefix .lane-line::before, -.tree-prefix .lane-joint::before { - content: ""; - position: absolute; - left: 0.6em; - top: 0; - bottom: 0; - border-left: 1px solid currentColor; -} -.tree-prefix .lane-joint-last::before { - bottom: auto; - height: 3.1em; -} -.tree-prefix .lane-joint::after { - content: ""; - position: absolute; - left: 0.6em; - top: 3.1em; - width: 2em; - border-top: 1px solid currentColor; -} .container-row:not(.tombstone) { display: flex; align-items: flex-start; diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 69c77bab..5281cf80 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -226,89 +226,27 @@ function derivePortConflicts(containers) { return out; } -// ─── topology tree ─────────────────────────────────────────────────────────── -// See docs/web-ui/dashboard.md::Topology tree for the rendering contract -// (forest walk, alphabetical sort, orphan + cycle handling). +// ─── container ordering ────────────────────────────────────────────────────── +// See docs/web-ui/dashboard.md::Topology tree for why this is a flat sort +// rather than the forest walk it used to be. -function buildAgentTree(containers) { +function sortedContainerRows(containers) { // Close any open context menu before replacing the DOM tree — the // previous dropdown element would otherwise be a stale reference. closeAllMenus(); - const byName = new Map(); - for (const c of containers) byName.set(c.name, c); - const children = new Map(); // parent_name -> [child_name, ...] - const roots = []; - for (const c of containers) { - const p = c.parent || null; - if (p == null || !byName.has(p)) { - roots.push(c.name); - } else { - const list = children.get(p) || []; - list.push(c.name); - children.set(p, list); - } - } - roots.sort(); - for (const list of children.values()) list.sort(); - const out = []; - const visited = new Set(); - function visit(name, depth, ancestorIsLast, isLast) { - if (visited.has(name)) return; - visited.add(name); - const c = byName.get(name); - if (!c) return; - out.push({ - container: c, - depth, - ancestorIsLast: [...ancestorIsLast], - isLast, - }); - const kids = children.get(name) || []; - kids.forEach((kid, i) => - visit(kid, depth + 1, [...ancestorIsLast, isLast], i === kids.length - 1), - ); - } - roots.forEach((name, i) => visit(name, 0, [], i === roots.length - 1)); - // Cycle safety: anything not reached lands at root level so no - // agent silently disappears when a config is malformed. - for (const c of containers) { - if (!visited.has(c.name)) visit(c.name, 0, [], true); - } - return out; -} -// Builds the .tree-prefix DOM for a row at the given depth. -// See docs/web-ui/dashboard.md::Topology tree for why this is DOM-painted -// (one positioned per lane) rather than text-glyph-painted. -function treePrefixDom({ depth, ancestorIsLast, isLast }) { - if (depth === 0) return null; - const prefix = el("span", { class: "tree-prefix", "aria-hidden": "true" }); - // Ancestor columns (depth 1..depth-1). Skip depth 0 (root has no - // continuation column — top-level rows are separated visually as - // top-level rows already). - for (let d = 1; d < depth; d++) { - const cls = ancestorIsLast[d] - ? "tree-lane lane-blank" - : "tree-lane lane-line"; - prefix.append(el("span", { class: cls })); - } - const jointCls = - "tree-lane lane-joint " + - (isLast ? "lane-joint-last" : "lane-joint-branch"); - prefix.append(el("span", { class: jointCls })); - return prefix; + return [...containers].sort((a, b) => a.name.localeCompare(b.name)); } // ─── container row ─────────────────────────────────────────────────────────── // Serialise the visible state of a container row into a stable string // for change-detection. Includes everything that affects what the row -// renders — container fields, derived pending/selection state, tree -// position, and link-base context. The async dashboard-state (nav -// strip, ctx badge, status text) is intentionally excluded: it -// populates in-place and is preserved when a row is reused. +// renders — container fields, derived pending/selection state, and +// link-base context. The async dashboard-state (nav strip, ctx badge, +// status text) is intentionally excluded: it populates in-place and is +// preserved when a row is reused. function containerRowFingerprint( c, - node, pending, opRunning, selected, @@ -326,9 +264,6 @@ function containerRowFingerprint( pending, opRunning, selected, - depth: node.depth, - isLast: node.isLast, - ancestorIsLast: node.ancestorIsLast, gatewayLinks, hostname, }); @@ -338,7 +273,7 @@ function containerRowFingerprint( // renderContainers can call this only for rows whose fingerprint // changed (keyed cache), skipping the build + async dashboard-state // fetch for stable rows. -function buildContainerLi(c, node, opts) { +function buildContainerLi(c, opts) { const { pending, opRunning, selected, url, containerBase, forgeBase, s } = opts; // A single `pending-running` class covers any active transient @@ -350,13 +285,6 @@ function buildContainerLi(c, node, opts) { (opRunning ? " pending-running" : "") + (selected ? " selected" : ""), }); - // Topology: depth contributes left-padding; the glyph string in - // the .tree-prefix span draws the ├─ / └─ joint + continuation - // lines (`│ `) for ancestors whose subtree extends below this - // row. A depth-0 row gets neither — no glyph, no indent. - if (node.depth > 0) li.dataset.depth = String(node.depth); - const prefix = treePrefixDom(node); - if (prefix) li.prepend(prefix); // Agent icon: 5em square wrapper with an absolutely-positioned // + fire-and-forget load with /favicon.svg fallback. The @@ -744,13 +672,12 @@ export function renderContainers(s) { // rather than emit a possibly-wrong one. const forgeBase = (s && s.forge_public_url) || null; const ul = existingUl ?? el("ul", { class: "containers" }); - const tree = buildAgentTree(containers); + const rows = sortedContainerRows(containers); // Build the ordered list of
  • elements, reusing cached rows // whose displayed state hasn't changed. const orderedLis = []; - for (const node of tree) { - const c = node.container; + for (const c of rows) { const url = gatewayLinks ? `/agent/${encodeURIComponent(c.name)}/` : `http://${hostname}:${c.port}/`; @@ -774,7 +701,6 @@ export function renderContainers(s) { const fp = containerRowFingerprint( c, - node, pending, opRunning, selected, @@ -790,7 +716,7 @@ export function renderContainers(s) { // intact from the previous build, avoiding a redundant fetch. li = cached.el; } else { - li = buildContainerLi(c, node, { + li = buildContainerLi(c, { pending, opRunning, selected,