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

Reply via email to