dashboard: flatten the agent list, drop dead topology-tree machinery (#4638)
The backend dropped the agent hierarchy's parent field, so every container is a root and buildAgentTree/treePrefixDom could only ever produce a single-level flat list — the .tree-prefix CSS lane rules already matched nothing. Replaced with sortedContainerRows, a plain alphabetical sort, and dropped the now-dead .tree-prefix/.tree-lane/ data-depth CSS and the depth/isLast/ancestorIsLast fields from the row fingerprint and buildContainerLi's signature. No visible behavior change - the rendered list was already flat, just via dead machinery. Docs updated to describe the simpler implementation directly instead of narrating the removal (kept the heading name since swarm.js still points a comment at it).
This commit is contained in:
parent
c9e12ffcc0
commit
6b5c8cc51a
3 changed files with 21 additions and 170 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <span> 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
|
||||
// <img> + 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 <li> 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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue