This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch feat/service-internal-topology in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 99d3b42048f8e01dd826e2f979efe74684354001 Author: Wu Sheng <[email protected]> AuthorDate: Tue Jun 9 21:25:38 2026 +0800 feat(layer): tiered internal-topology layout — pods by call-depth, wrap >4/tier into columns of 4 Lay each cluster box out by call depth: rank pods from the sources of the intra-cluster edges and place them left-to-right by rank, so a hot -> warm -> cold lifecycle chain reads as tiers. Pods stack vertically within a tier; a tier with more than four pods wraps into adjacent stacked sub-columns of four. The cluster boundary is computed from live node positions, so it re-flows when a pod is dragged. Bump the General-layer preview mock to the larger banyandb-cluster-large variant (6 hot / 3 warm / 3 cold) so the per-tier column wrap is exercisable in preview. --- CHANGELOG.md | 6 ++++ apps/bff/src/bundled_templates/layers/general.json | 2 +- .../bff/src/logic/layers/mock-internal-topology.ts | 9 ++--- .../LayerServiceInternalTopologyView.vue | 41 ++++++++++++++-------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b813692..aa6983e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,12 @@ packages) plus the BFF's `HORIZON_VERSION` default. A self-contained **preview mock** (a BanyanDB-shaped liaison + hot/warm/cold data cluster) is wired onto the **General** layer so the model is previewable before real data exists. +- **Tiered layout + draggable pods.** Each cluster box lays its pods out by + call depth — sources on the left, the pods they call to the right — so a + hot → warm → cold lifecycle chain reads as left-to-right tiers. Pods stack + vertically within a tier, and a tier with more than four pods wraps into + additional stacked columns of four. Drag any pod to rearrange; its cluster + box re-flows to keep every node enclosed. ### API dependency diff --git a/apps/bff/src/bundled_templates/layers/general.json b/apps/bff/src/bundled_templates/layers/general.json index 23f5487..de8dfa9 100644 --- a/apps/bff/src/bundled_templates/layers/general.json +++ b/apps/bff/src/bundled_templates/layers/general.json @@ -959,7 +959,7 @@ ] }, "serviceInternalTopology": { - "mock": "banyandb-cluster", + "mock": "banyandb-cluster-large", "clusterBy": { "kind": "attribute", "attribute": "node_role", "alias": "role" }, "siblingBy": { "kind": "attribute", "attribute": "pod", "alias": "pod" }, "roleBy": { "kind": "attribute", "attribute": "container", "alias": "container" }, diff --git a/apps/bff/src/logic/layers/mock-internal-topology.ts b/apps/bff/src/logic/layers/mock-internal-topology.ts index 2a35542..62fe91a 100644 --- a/apps/bff/src/logic/layers/mock-internal-topology.ts +++ b/apps/bff/src/logic/layers/mock-internal-topology.ts @@ -152,14 +152,15 @@ function dataPodN(tier: 'hot' | 'warm' | 'cold', idx: number, diskPct: number, w ]; } -/** Larger variant — 2 liaison + 4 hot + 2 warm + 2 cold data pods — to show - * the layout with multiple pods per tier (a hot row of 4, warm/cold rows of - * 2). Lifecycle tree: hot[i] → warm[i mod 2] → cold[j mod 2]. */ +/** Larger variant — 2 liaison + 6 hot + 3 warm + 3 cold data pods — to show + * the layout with multiple pods per tier AND the >4-per-column wrap (the hot + * tier of 6 splits into two stacked sub-columns of 4 + 2). Lifecycle tree: + * hot[i] → warm[i mod warm] → cold[j mod cold]. */ export function buildMockInternalTopologyLarge(serviceId: string, serviceName: string | null): { nodes: ServiceInternalTopologyNode[]; calls: ServiceInternalTopologyCall[]; } { - const counts: Record<'hot' | 'warm' | 'cold', number> = { hot: 4, warm: 2, cold: 2 }; + const counts: Record<'hot' | 'warm' | 'cold', number> = { hot: 6, warm: 3, cold: 3 }; const disk = { hot: 71, warm: 48, cold: 86 } as const; const write = { hot: 940, warm: 120, cold: 12 } as const; const nodes: ServiceInternalTopologyNode[] = [...liaisonPod(0, 1820, 0.2), ...liaisonPod(1, 1640, 0.9)]; diff --git a/apps/ui/src/layer/service-map/LayerServiceInternalTopologyView.vue b/apps/ui/src/layer/service-map/LayerServiceInternalTopologyView.vue index 805bfd7..627f91a 100644 --- a/apps/ui/src/layer/service-map/LayerServiceInternalTopologyView.vue +++ b/apps/ui/src/layer/service-map/LayerServiceInternalTopologyView.vue @@ -353,17 +353,24 @@ const layout = computed<Layout>(() => { const podById = new Map(cl.pods.map((p) => [podIdOf(cl.key, p.siblingKey), p])); const ids = [...podById.keys()]; const rank = rankPods(ids, intraByCluster.get(cl.key ?? '') ?? []); - // Group pods into rows by rank (rank 0 = top); within a rank order by main - // name and spread horizontally, centred — so a chain flows straight down - // and same-tier pods (e.g. 4 hot) sit side-by-side on one row. + // Group pods into COLUMNS by rank (rank = tier: hot→warm→cold left→right); + // within a column order by main name and stack VERTICALLY, centred — so a + // tier's nodes (e.g. 4 hot) form a vertical column and the chain flows + // left→right. Mirrors the service map's BFS column layout. const maxRank = Math.max(0, ...ids.map((id) => rank.get(id) ?? 0)); - const rankRows: string[][] = Array.from({ length: maxRank + 1 }, () => []); - for (const id of ids) rankRows[rank.get(id) ?? 0].push(id); - for (const row of rankRows) row.sort((a, b) => podById.get(a)!.main.name.localeCompare(podById.get(b)!.main.name)); - const maxRowLen = Math.max(1, ...rankRows.map((r) => r.length)); + const rankCols: string[][] = Array.from({ length: maxRank + 1 }, () => []); + for (const id of ids) rankCols[rank.get(id) ?? 0].push(id); + for (const col of rankCols) col.sort((a, b) => podById.get(a)!.main.name.localeCompare(podById.get(b)!.main.name)); + // A tier column holds at most COL_CAP pods; overflow wraps into extra + // sub-columns (so 9 hot pods → 3 sub-columns of 4/4/1). Each rank reserves + // as many sub-columns as it needs; later ranks shift right accordingly. + const COL_CAP = 4; + const subColsPerRank = rankCols.map((c) => Math.max(1, Math.ceil(c.length / COL_CAP))); + const totalSubCols = subColsPerRank.reduce((a, b) => a + b, 0); + const maxColLen = Math.min(COL_CAP, Math.max(1, ...rankCols.map((c) => c.length))); const headH = showBoxes ? HEAD_H : 0; - const boxW = maxRowLen * POD_DX + CLUSTER_PAD * 2; - const boxH = (maxRank + 1) * POD_DY + CLUSTER_PAD * 2 + headH; + const boxW = totalSubCols * POD_DX + CLUSTER_PAD * 2; + const boxH = maxColLen * POD_DY + CLUSTER_PAD * 2 + headH; if (cursorX > 0 && cursorX + boxW > MAX_ROW_W) { cursorX = 0; cursorY += rowMaxH + CLUSTER_GAP_Y; @@ -371,19 +378,23 @@ const layout = computed<Layout>(() => { } const boxX = cursorX; const boxY = cursorY; - const innerCx = boxX + boxW / 2; - rankRows.forEach((row, r) => { - const n = row.length; - row.forEach((pid, k) => { + const colMidY = boxY + headH + CLUSTER_PAD + (maxColLen * POD_DY) / 2; + let subColBase = 0; + rankCols.forEach((col, r) => { + col.forEach((pid, k) => { const pod = podById.get(pid)!; - const cx = innerCx + (k - (n - 1) / 2) * POD_DX; - const cy = boxY + headH + CLUSTER_PAD + r * POD_DY + POD_DY / 2 - 10; + const subCol = Math.floor(k / COL_CAP); + const rowInCol = k % COL_CAP; + const nInSub = Math.min(COL_CAP, col.length - subCol * COL_CAP); + const cx = boxX + CLUSTER_PAD + (subColBase + subCol) * POD_DX + POD_DX / 2; + const cy = colMidY + (rowInCol - (nInSub - 1) / 2) * POD_DY; pos.set(pod.main.id, { cx, cy, r: MAIN_R }); pod.siblings.slice(0, SIB_ANGLES.length).forEach((sib, j) => { const a = SIB_ANGLES[j]; pos.set(sib.id, { cx: cx + Math.cos(a) * SIB_DIST, cy: cy + Math.sin(a) * SIB_DIST, r: SIB_R }); }); }); + subColBase += subColsPerRank[r]; }); rects.push({ key: cl.key, label: cl.label, x: boxX, y: boxY, w: boxW, h: boxH, boxed: showBoxes }); cursorX += boxW + CLUSTER_GAP_X;
