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;

Reply via email to