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;