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 fbbb4f3 ui: sticky service selection + tooltip page-coord fix +
mesh_dp top-row cards
fbbb4f3 is described below
commit fbbb4f3ae7dbe45de6e7306d1b24920a13b36336
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 11:01:57 2026 +0800
ui: sticky service selection + tooltip page-coord fix + mesh_dp top-row
cards
- useSelectedService: setSelected gains `keepNarrower` so the periodic
service-auto-repair (URL pick falls out of the sampled subset) no
longer drops the user's `?instance=` / `?endpoint=` as a side-effect.
Explicit picker clicks still drop them — that's the right semantics
when the user actually changes service.
- LayerShell + LayerDashboardsView pass `keepNarrower: Boolean(id)` on
the repair path; the fresh-seed path keeps default behaviour so first
visit still auto-picks cleanly (so11y_java_agent etc).
- TimeChart tooltip: with appendToBody:true echarts puts the popover in
<body>, so the returned [x,y] must be page-absolute. Clamp in
viewport coords, then add window.scrollX/Y at return — the previous
patch returned viewport coords and the tooltip ended up off-screen.
- mesh_dp instance dashboard: promote bug_failures / membership_healthy
/ worker_threads / up_rq_active to top-row cards (latest(...) +
rowSpan:1), with the worker_threads current-vs-max comparison moved
to a dedicated line widget so the gauge feel stays.
---
apps/bff/src/bundled_templates/layers/mesh_dp.json | 163 +++++++++++----------
apps/ui/src/components/charts/TimeChart.vue | 35 ++---
apps/ui/src/layer/LayerShell.vue | 12 +-
apps/ui/src/layer/useSelectedService.ts | 22 ++-
.../render/layer-dashboard/LayerDashboardsView.vue | 6 +-
5 files changed, 134 insertions(+), 104 deletions(-)
diff --git a/apps/bff/src/bundled_templates/layers/mesh_dp.json
b/apps/bff/src/bundled_templates/layers/mesh_dp.json
index 5603118..445c340 100644
--- a/apps/bff/src/bundled_templates/layers/mesh_dp.json
+++ b/apps/bff/src/bundled_templates/layers/mesh_dp.json
@@ -32,13 +32,78 @@
"dashboards": {
"instance": [
{
- "id": "up_rq_active",
+ "id": "bug_failures_card",
+ "title": "Bug Failures",
+ "tip": "Envoy internal bug-failure counter — non-zero indicates an
assertion / debug check tripped (envoy_bug_failures).",
+ "type": "card",
+ "unit": "count",
+ "expressions": [
+ "latest(envoy_bug_failures)"
+ ],
+ "span": 3,
+ "rowSpan": 1,
+ "format": "int"
+ },
+ {
+ "id": "membership_healthy_card",
+ "title": "Membership Healthy",
+ "tip": "Healthy endpoints across all clusters
(envoy_cluster_membership_healthy).",
+ "type": "card",
+ "unit": "endpoints",
+ "expressions": [
+ "latest(envoy_cluster_membership_healthy)"
+ ],
+ "span": 3,
+ "rowSpan": 1,
+ "format": "int"
+ },
+ {
+ "id": "worker_threads_card",
+ "title": "Worker Threads",
+ "tip": "Concurrent worker threads currently in use
(envoy_worker_threads).",
+ "type": "card",
+ "unit": "threads",
+ "expressions": [
+ "latest(envoy_worker_threads)"
+ ],
+ "span": 3,
+ "rowSpan": 1,
+ "format": "int"
+ },
+ {
+ "id": "up_rq_active_card",
"title": "Upstream Request Active",
- "tip": "Total active upstream requests from this Envoy sidecar's
clusters (envoy_cluster_up_rq_active).",
+ "tip": "Total active upstream requests across this Envoy's clusters
(envoy_cluster_up_rq_active).",
+ "type": "card",
+ "unit": "reqs",
+ "expressions": [
+ "latest(envoy_cluster_up_rq_active)"
+ ],
+ "span": 3,
+ "rowSpan": 1,
+ "format": "int"
+ },
+ {
+ "id": "up_cx_active",
+ "title": "Upstream Connection Active",
+ "tip": "Active upstream connections (envoy_cluster_up_cx_active).",
+ "type": "line",
+ "unit": "conns",
+ "expressions": [
+ "envoy_cluster_up_cx_active"
+ ],
+ "span": 3,
+ "rowSpan": 2,
+ "format": "int"
+ },
+ {
+ "id": "up_rq_pending",
+ "title": "Upstream Request Pending",
+ "tip": "Requests pending in upstream queues
(envoy_cluster_up_rq_pending_active).",
"type": "line",
"unit": "reqs",
"expressions": [
- "envoy_cluster_up_rq_active"
+ "envoy_cluster_up_rq_pending_active"
],
"span": 3,
"rowSpan": 2,
@@ -63,41 +128,46 @@
"format": "int"
},
{
- "id": "membership_healthy",
- "title": "Membership Healthy",
- "tip": "Healthy endpoints across all outbound/inbound clusters
(envoy_cluster_membership_healthy).",
+ "id": "up_cx_incr",
+ "title": "Upstream Connection Increase",
+ "tip": "New upstream connections opened per minute
(envoy_cluster_up_cx_incr).",
"type": "line",
- "unit": "endpoints",
+ "unit": "/min",
"expressions": [
- "envoy_cluster_membership_healthy"
+ "envoy_cluster_up_cx_incr"
],
"span": 3,
"rowSpan": 2,
"format": "int"
},
{
- "id": "up_cx_incr",
- "title": "Upstream Connection Increase",
- "tip": "New upstream connections opened per minute
(envoy_cluster_up_cx_incr).",
+ "id": "up_rq_incr",
+ "title": "Upstream Request Increase",
+ "tip": "New upstream requests per minute (envoy_cluster_up_rq_incr).",
"type": "line",
"unit": "/min",
"expressions": [
- "envoy_cluster_up_cx_incr"
+ "envoy_cluster_up_rq_incr"
],
"span": 3,
"rowSpan": 2,
"format": "int"
},
{
- "id": "up_rq_pending",
- "title": "Upstream Request Pending",
- "tip": "Requests pending in upstream queues
(envoy_cluster_up_rq_pending_active).",
+ "id": "worker_threads_trend",
+ "title": "Worker Threads (current vs max)",
+ "tip": "Concurrent worker threads in use vs the window max
(envoy_worker_threads / envoy_worker_threads_max).",
"type": "line",
- "unit": "reqs",
+ "unit": "threads",
"expressions": [
- "envoy_cluster_up_rq_pending_active"
+ "envoy_worker_threads",
+ "envoy_worker_threads_max"
],
- "span": 3,
+ "expressionLabels": [
+ "current",
+ "max"
+ ],
+ "span": 6,
"rowSpan": 2,
"format": "int"
},
@@ -125,63 +195,6 @@
],
"span": 6,
"rowSpan": 3
- },
- {
- "id": "bug_failures",
- "title": "Bug Failures",
- "tip": "Envoy internal bug-failure counter \u2014 non-zero indicates
an assertion / debug check tripped (envoy_bug_failures).",
- "type": "line",
- "unit": "count",
- "expressions": [
- "envoy_bug_failures"
- ],
- "span": 3,
- "rowSpan": 2,
- "format": "int"
- },
- {
- "id": "worker_threads",
- "title": "Worker Threads",
- "tip": "Concurrent worker threads in use vs the window max
(envoy_worker_threads / envoy_worker_threads_max).",
- "type": "line",
- "unit": "threads",
- "expressions": [
- "envoy_worker_threads",
- "envoy_worker_threads_max"
- ],
- "expressionLabels": [
- "current",
- "max"
- ],
- "span": 3,
- "rowSpan": 2,
- "format": "int"
- },
- {
- "id": "up_cx_active",
- "title": "Upstream Connection Active",
- "tip": "Active upstream connections (envoy_cluster_up_cx_active).",
- "type": "line",
- "unit": "conns",
- "expressions": [
- "envoy_cluster_up_cx_active"
- ],
- "span": 3,
- "rowSpan": 2,
- "format": "int"
- },
- {
- "id": "up_rq_incr",
- "title": "Upstream Request Increase",
- "tip": "New upstream requests per minute (envoy_cluster_up_rq_incr).",
- "type": "line",
- "unit": "/min",
- "expressions": [
- "envoy_cluster_up_rq_incr"
- ],
- "span": 3,
- "rowSpan": 2,
- "format": "int"
}
]
},
diff --git a/apps/ui/src/components/charts/TimeChart.vue
b/apps/ui/src/components/charts/TimeChart.vue
index aba48e8..c62b25a 100644
--- a/apps/ui/src/components/charts/TimeChart.vue
+++ b/apps/ui/src/components/charts/TimeChart.vue
@@ -131,8 +131,15 @@ function buildOption(): echarts.EChartsCoreOption {
// the chart's own bbox, which doesn't help when the chart sits
// near the right / bottom screen edge — the tooltip still spills
// off-screen. This callback flips the tooltip to the opposite
- // side of the cursor whenever it would overflow the viewport,
- // and never returns a coord with negative top/left.
+ // side of the cursor whenever it would overflow the viewport.
+ //
+ // Coord systems matter here:
+ // - `point` is chart-container-relative (echarts always passes it
+ // that way, regardless of appendToBody).
+ // - Viewport bounds use clientWidth/Height (no scroll).
+ // - The returned [x, y] feeds `style.left/top` on the tooltip
+ // DOM. With appendToBody:true the tooltip is in <body>, so the
+ // returned coords must be PAGE-absolute (viewport + scroll).
position(
point: [number, number],
_params: unknown,
@@ -140,27 +147,21 @@ function buildOption(): echarts.EChartsCoreOption {
_rect: unknown,
size: { contentSize: [number, number]; viewSize: [number, number] },
): [number, number] {
- // `point` is mouse pos in the chart's local coords; we need
- // page coords to compare against viewport bounds. The chart's
- // bounding rect on the page gives us the offset.
const chartRect = container.value?.getBoundingClientRect();
- const pageX = (chartRect?.left ?? 0) + point[0];
- const pageY = (chartRect?.top ?? 0) + point[1];
+ const viewportX = (chartRect?.left ?? 0) + point[0];
+ const viewportY = (chartRect?.top ?? 0) + point[1];
const [tw, th] = size.contentSize;
const vw = document.documentElement.clientWidth;
const vh = document.documentElement.clientHeight;
const margin = 8;
const offset = 12;
- let x = pageX + offset;
- if (x + tw > vw - margin) x = pageX - tw - offset;
- if (x < margin) x = margin;
- let y = pageY + offset;
- if (y + th > vh - margin) y = pageY - th - offset;
- if (y < margin) y = margin;
- // With appendToBody:true, echarts treats returned [x, y] as
- // page-absolute coords (positions the tooltip element in
- // document.body), so we return pixel values.
- return [x, y];
+ let vx = viewportX + offset;
+ if (vx + tw > vw - margin) vx = viewportX - tw - offset;
+ if (vx < margin) vx = margin;
+ let vy = viewportY + offset;
+ if (vy + th > vh - margin) vy = viewportY - th - offset;
+ if (vy < margin) vy = margin;
+ return [vx + window.scrollX, vy + window.scrollY];
},
valueFormatter: (v: unknown) =>
typeof v === 'number' && Number.isFinite(v)
diff --git a/apps/ui/src/layer/LayerShell.vue b/apps/ui/src/layer/LayerShell.vue
index f568b00..9854d32 100644
--- a/apps/ui/src/layer/LayerShell.vue
+++ b/apps/ui/src/layer/LayerShell.vue
@@ -151,10 +151,12 @@ const selectedName = computed(() => {
// say) can flip the same meta flag without touching this file.
const viewOwnsServiceSelector = computed(() =>
Boolean(route.meta?.ownsServiceSelector));
-// Keep the URL-backed service selection honest for every page that
-// uses the shell picker. A stale `?service=` can survive navigation or
-// manual URL entry; the switch label used to fall back visually to the
-// first row while the metric query still waited for a valid service.
+// Seed `?service=` on first visit and repair a stale URL pick (a
+// bookmark / cross-layer link whose service no longer exists). The
+// repair call passes `keepNarrower: true` so the auto-rewrite during
+// a periodic refetch can't silently drop a still-valid `?instance=` /
+// `?endpoint=` — that drop is reserved for the user explicitly picking
+// a different service in the dropdown (pickService).
watch(
[sampledServices, selectedId, viewOwnsServiceSelector],
([rows, id, ownsSelector]) => {
@@ -162,7 +164,7 @@ watch(
const first = rows[0];
if (!first) return;
if (!id || !rows.some((s) => s.serviceId === id)) {
- setSelected(first.serviceId);
+ setSelected(first.serviceId, { keepNarrower: Boolean(id) });
}
},
{ immediate: true },
diff --git a/apps/ui/src/layer/useSelectedService.ts
b/apps/ui/src/layer/useSelectedService.ts
index a8d84b5..4f0c277 100644
--- a/apps/ui/src/layer/useSelectedService.ts
+++ b/apps/ui/src/layer/useSelectedService.ts
@@ -36,17 +36,27 @@ export function useSelectedService() {
return null;
});
- function setSelected(id: string | null): void {
+ /**
+ * Update the URL-backed service selection.
+ *
+ * `opts.keepNarrower` controls whether sibling `?instance=` /
+ * `?endpoint=` params survive. Default `false` matches the user
+ * clicking a different service in the picker — those narrower picks
+ * belong to the OLD service and have to go. Auto-repair callers (the
+ * shell's "URL points at a service no longer in the sampled subset"
+ * watch) pass `true` so the URL's existing instance/endpoint isn't
+ * blown away as a side-effect of a service-side refresh.
+ */
+ function setSelected(id: string | null, opts: { keepNarrower?: boolean } =
{}): void {
const next = { ...route.query };
const current = typeof route.query.service === 'string' ?
route.query.service : null;
if (id === current) return;
if (id) next.service = id;
else delete next.service;
- // Instance / endpoint choices are derived from the selected service.
- // When the service changes, drop the narrower entity so each
- // dashboard can auto-pick from the new service's own list.
- delete next.instance;
- delete next.endpoint;
+ if (!opts.keepNarrower) {
+ delete next.instance;
+ delete next.endpoint;
+ }
// `replace` instead of `push` — switching services shouldn't bloat
// the browser back stack with N entries.
void router.replace({ path: route.path, query: next });
diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
index 800ab83..d5c55c9 100644
--- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
@@ -140,8 +140,12 @@ const landingRows = computed(() =>
landing.data.value?.sampledRows ?? landing.ro
watch(landingRows, (rows) => {
const first = rows[0];
if (!first) return;
+ // `keepNarrower: true` when repairing an existing URL pick so a
+ // periodic landing refetch can't silently drop the operator's
+ // `?instance=` / `?endpoint=` — only a fresh seed (no `?service=`
+ // yet) is allowed to start from a clean slate.
if (!selectedId.value || !rows.some((r) => r.serviceId ===
selectedId.value)) {
- setSelectedService(first.serviceId);
+ setSelectedService(first.serviceId, { keepNarrower:
Boolean(selectedId.value) });
}
}, { immediate: true });
// Drop the stale instance whenever the service changes — the new