This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new dcfd08d  ui/bff: process topology — topo-aligned fonts, 
boundary-following peers, wide client|server edge dashboard, fuller default 
ProcessRelation set
dcfd08d is described below

commit dcfd08d4a86744f1d144a0388685858597c09fb3
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 11:51:55 2026 +0800

    ui/bff: process topology — topo-aligned fonts, boundary-following peers,
    wide client|server edge dashboard, fuller default ProcessRelation set
    
    Topology graph:
      - Node + boundary labels bumped to 13px mono to match the service-map
        topology's label scale.
      - External peers now ride the pod-boundary centre: each freezes its
        offset from the boundary at layout time, and `redrawBoundary` (run on
        every inside-node drag tick) repositions them so they keep orbiting
        the pod instead of being left behind / overlapped when the boundary
        reshapes.
    
    Edge dashboard modal:
      - Re-laid out as a wide (1320px / 96vw) client | server side-by-side
        grid. Each column stacks its metric widgets; matching ids align
        row-for-row across the two columns. Per-side accent vs info heading.
    
    Default ProcessRelation metric set (operator-editable in admin):
      - Expanded the TCP set to write/read CPM, write/read package size (KB,
        bytes/1024), write/read avg time (ms, exe_time/1000000), connect &
        close CPM — paired client/server by id. Validated live against the
        demo (write_kb ≈ 0.84 KB, write_avg_ms ≈ 0.1 ms for envoy→pilot-agent).
---
 apps/bff/src/logic/layers/loader.ts                | 32 ++++++----
 .../layer/profiling/LayerNetworkProfilingView.vue  | 68 ++++++++++++----------
 .../src/layer/profiling/ProcessTopologyGraph.vue   | 31 ++++++++--
 3 files changed, 83 insertions(+), 48 deletions(-)

diff --git a/apps/bff/src/logic/layers/loader.ts 
b/apps/bff/src/logic/layers/loader.ts
index 68c9350..153d823 100644
--- a/apps/bff/src/logic/layers/loader.ts
+++ b/apps/bff/src/logic/layers/loader.ts
@@ -320,21 +320,29 @@ export const BOOSTER_ENDPOINT_DEP_DEFAULTS: 
EndpointDependencyConfig = {
  * `*_total_bytes` are cumulative counters summed over the window.
  */
 export const BOOSTER_PROCESS_TOPOLOGY_DEFAULTS: ProcessTopologyConfig = {
+  // Default ProcessRelation TCP metric set — fully operator-editable via
+  // the admin network-profiling config. ids pair across client/server so
+  // the edge dashboard renders matching rows side-by-side; bytes are
+  // /1024 → KB and exe times /1000000 → ms for readable units.
   edgeClientMetrics: [
-    { id: 'write_cpm', label: 'Write CPM', mqe: 
'process_relation_client_write_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'read_cpm', label: 'Read CPM', mqe: 
'process_relation_client_read_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'write_bytes', label: 'Write bytes', mqe: 
'process_relation_client_write_total_bytes', unit: 'B', aggregation: 'sum' },
-    { id: 'read_bytes', label: 'Read bytes', mqe: 
'process_relation_client_read_total_bytes', unit: 'B', aggregation: 'sum' },
-    { id: 'connect_cpm', label: 'Connect CPM', mqe: 
'process_relation_client_connect_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'close_cpm', label: 'Close CPM', mqe: 
'process_relation_client_close_cpm', unit: 'cpm', aggregation: 'avg' },
+    { id: 'write_cpm', label: 'Write OP / min', mqe: 
'process_relation_client_write_cpm', aggregation: 'avg' },
+    { id: 'read_cpm', label: 'Read OP / min', mqe: 
'process_relation_client_read_cpm', aggregation: 'avg' },
+    { id: 'write_kb', label: 'Write package size', mqe: 
'process_relation_client_write_total_bytes/1024', unit: 'KB', aggregation: 
'avg' },
+    { id: 'read_kb', label: 'Read package size', mqe: 
'process_relation_client_read_total_bytes/1024', unit: 'KB', aggregation: 'avg' 
},
+    { id: 'write_avg_ms', label: 'Write OP avg time', mqe: 
'process_relation_client_write_avg_exe_time/1000000', unit: 'ms', aggregation: 
'avg' },
+    { id: 'read_avg_ms', label: 'Read OP avg time', mqe: 
'process_relation_client_read_avg_exe_time/1000000', unit: 'ms', aggregation: 
'avg' },
+    { id: 'connect_cpm', label: 'Connect OP / min', mqe: 
'process_relation_client_connect_cpm', aggregation: 'avg' },
+    { id: 'close_cpm', label: 'Close OP / min', mqe: 
'process_relation_client_close_cpm', aggregation: 'avg' },
   ],
   edgeServerMetrics: [
-    { id: 'write_cpm', label: 'Write CPM', mqe: 
'process_relation_server_write_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'read_cpm', label: 'Read CPM', mqe: 
'process_relation_server_read_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'write_bytes', label: 'Write bytes', mqe: 
'process_relation_server_write_total_bytes', unit: 'B', aggregation: 'sum' },
-    { id: 'read_bytes', label: 'Read bytes', mqe: 
'process_relation_server_read_total_bytes', unit: 'B', aggregation: 'sum' },
-    { id: 'connect_cpm', label: 'Connect CPM', mqe: 
'process_relation_server_connect_cpm', unit: 'cpm', aggregation: 'avg' },
-    { id: 'close_cpm', label: 'Close CPM', mqe: 
'process_relation_server_close_cpm', unit: 'cpm', aggregation: 'avg' },
+    { id: 'write_cpm', label: 'Write OP / min', mqe: 
'process_relation_server_write_cpm', aggregation: 'avg' },
+    { id: 'read_cpm', label: 'Read OP / min', mqe: 
'process_relation_server_read_cpm', aggregation: 'avg' },
+    { id: 'write_kb', label: 'Write package size', mqe: 
'process_relation_server_write_total_bytes/1024', unit: 'KB', aggregation: 
'avg' },
+    { id: 'read_kb', label: 'Read package size', mqe: 
'process_relation_server_read_total_bytes/1024', unit: 'KB', aggregation: 'avg' 
},
+    { id: 'write_avg_ms', label: 'Write OP avg time', mqe: 
'process_relation_server_write_avg_exe_time/1000000', unit: 'ms', aggregation: 
'avg' },
+    { id: 'read_avg_ms', label: 'Read OP avg time', mqe: 
'process_relation_server_read_avg_exe_time/1000000', unit: 'ms', aggregation: 
'avg' },
+    { id: 'connect_cpm', label: 'Connect OP / min', mqe: 
'process_relation_server_connect_cpm', aggregation: 'avg' },
+    { id: 'close_cpm', label: 'Close OP / min', mqe: 
'process_relation_server_close_cpm', aggregation: 'avg' },
   ],
 };
 
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index f47cdc9..91b4998 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -396,28 +396,30 @@ function fmtTime(ms: number): string {
       <div class="dlg-body edge-dlg-body">
         <div v-if="relationLoading" class="muted">Reading process-relation 
metrics…</div>
         <div v-else-if="relationError" class="banner err">{{ relationError 
}}</div>
-        <template v-else-if="relationMetrics">
+        <!-- Left/right split: client side | server side. Each column
+             stacks its metric widgets; matching ids line up row-for-row.
+             The metric set is operator-configurable in the admin. -->
+        <div v-else-if="relationMetrics" class="edge-cols">
           <section
             v-for="side in (['client', 'server'] as const)"
             :key="side"
-            class="edge-side"
+            class="edge-col"
           >
-            <h5 class="edge-side-head">{{ side }} side</h5>
-            <div class="edge-grid">
-              <div v-for="m in relationMetrics[side]" :key="m.id" 
class="edge-widget sw-card">
-                <div class="ew-head">
-                  <span class="ew-label">{{ m.label }}</span>
-                  <span class="ew-val mono">{{ 
fmtMetric(latestValue(m.values), m.unit) }}</span>
-                </div>
-                <TimeChart
-                  :series="[{ label: m.label, data: m.values, unit: m.unit }]"
-                  :height="120"
-                  :unit="m.unit"
-                />
+            <h5 class="edge-col-head" :class="side">{{ side === 'client' ? 
'Client side' : 'Server side' }}</h5>
+            <div v-if="!relationMetrics[side].length" class="muted sm">No {{ 
side }} metrics configured.</div>
+            <div v-for="m in relationMetrics[side]" :key="m.id" 
class="edge-widget sw-card">
+              <div class="ew-head">
+                <span class="ew-label">{{ m.label }}</span>
+                <span class="ew-val mono">{{ fmtMetric(latestValue(m.values), 
m.unit) }}</span>
               </div>
+              <TimeChart
+                :series="[{ label: m.label, data: m.values, unit: m.unit }]"
+                :height="130"
+                :unit="m.unit"
+              />
             </div>
           </section>
-        </template>
+        </div>
       </div>
     </div>
   </div>
@@ -744,8 +746,8 @@ function fmtTime(ms: number): string {
   font-size: 10.5px;
   color: var(--sw-fg-1);
 }
-/* Edge dashboard modal — full process-relation widget grid. */
-.edge-dlg { width: 980px; max-width: 94vw; max-height: 88vh; }
+/* Edge dashboard modal — wide, client | server side-by-side. */
+.edge-dlg { width: 1320px; max-width: 96vw; max-height: 90vh; }
 .edge-dlg-title {
   display: flex;
   align-items: center;
@@ -764,20 +766,24 @@ function fmtTime(ms: number): string {
   padding: 1px 8px;
 }
 .edge-dlg-body { overflow-y: auto; padding: 12px 14px; }
-.edge-side { margin-bottom: 14px; }
-.edge-side-head {
-  margin: 0 0 8px;
-  font-size: 9.5px;
-  font-weight: 600;
-  letter-spacing: 0.08em;
-  text-transform: uppercase;
-  color: var(--sw-fg-3);
-}
-.edge-grid {
+.edge-cols {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
-  gap: 10px;
+  grid-template-columns: 1fr 1fr;
+  gap: 14px;
+  align-items: start;
 }
+.edge-col { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
+.edge-col-head {
+  margin: 0;
+  padding-bottom: 6px;
+  border-bottom: 2px solid var(--sw-line);
+  font-size: 11px;
+  font-weight: 700;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+}
+.edge-col-head.client { color: var(--sw-accent); border-bottom-color: 
var(--sw-accent); }
+.edge-col-head.server { color: var(--sw-info, #5a9cf8); border-bottom-color: 
var(--sw-info, #5a9cf8); }
 .edge-widget { padding: 8px 10px; }
 .ew-head {
   display: flex;
@@ -785,9 +791,9 @@ function fmtTime(ms: number): string {
   justify-content: space-between;
   margin-bottom: 4px;
 }
-.ew-label { font-size: 11px; color: var(--sw-fg-2); }
+.ew-label { font-size: 11.5px; color: var(--sw-fg-1); font-weight: 600; }
 .ew-val {
-  font-size: 11px;
+  font-size: 11.5px;
   color: var(--sw-fg-0);
   font-family: var(--sw-mono);
   font-variant-numeric: tabular-nums;
diff --git a/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue 
b/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
index 2e8e22a..158f56a 100644
--- a/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
+++ b/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
@@ -37,7 +37,10 @@ import * as d3 from 'd3';
 import type { ProcessCall, ProcessNode } from '@/api/client';
 
 interface Pt { x: number; y: number }
-type PositionedNode = ProcessNode & Pt;
+// `_ox/_oy` = offset from the pod-boundary centre, stored for external
+// peers so they rigidly follow the boundary when it's recomputed during
+// an inside-node drag (they orbit the pod, never end up inside it).
+type PositionedNode = ProcessNode & Pt & { _ox?: number; _oy?: number };
 interface PositionedCall {
   id: string;
   source: PositionedNode;
@@ -138,7 +141,15 @@ function layout(o: Pt): PositionedNode[] {
     n.y = p.y;
   });
 
-  return [...inside, ...outside];
+  // Freeze each external peer's offset from the initial boundary centre
+  // so it follows the boundary when that's recomputed mid-drag.
+  positioned = [...inside, ...outside];
+  const b = insideBoundary();
+  for (const n of outside) {
+    n._ox = n.x - b.cx;
+    n._oy = n.y - b.cy;
+  }
+  return positioned;
 }
 
 /** Max distance from `o` to any inside cell centre. */
@@ -234,6 +245,13 @@ function redrawBoundary(): void {
   const b = insideBoundary();
   boundarySel?.attr('d', hexCellPath(b.cx, b.cy, b.r));
   boundaryLabelSel?.attr('x', b.cx).attr('y', b.cy + b.r + 16);
+  // External peers ride the boundary centre so they keep orbiting the
+  // pod as inside cells are dragged around.
+  for (const n of positioned) {
+    if (isInside(n) || n._ox === undefined || n._oy === undefined) continue;
+    n.x = b.cx + n._ox;
+    n.y = b.cy + n._oy;
+  }
 }
 function refreshPositions(): void {
   edgeSel?.attr('d', edgePath);
@@ -304,7 +322,7 @@ function render(): void {
     .attr('text-anchor', 'middle')
     .attr('fill', 'var(--sw-fg-2, #b4b7c2)')
     .style('font-family', 'var(--sw-mono, monospace)')
-    .style('font-size', '11px')
+    .style('font-size', '13px')
     .text(instanceLabel());
 
   // Edges (animated flow).
@@ -383,8 +401,11 @@ function render(): void {
         .on('drag', (ev, d) => {
           d.x = ev.x;
           d.y = ev.y;
-          refreshPositions();
+          // Dragging an inside cell reshapes the boundary, which in turn
+          // repositions the external peers — recompute that first, then
+          // flush all transforms in one pass.
           if (isInside(d)) redrawBoundary();
+          refreshPositions();
         }) as never,
     );
   nodeSel
@@ -401,7 +422,7 @@ function render(): void {
     .attr('text-anchor', 'middle')
     .attr('fill', 'var(--sw-fg-1, #d4d6dd)')
     .style('font-family', 'var(--sw-mono, monospace)')
-    .style('font-size', '10px')
+    .style('font-size', '13px')
     .text((d) => {
       const base = d.name.split('.')[0];
       return base.length > 14 ? `${base.slice(0, 14)}…` : base;

Reply via email to