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 7985a48 ui dashboards: strict cascade for instance/endpoint pickers +
URL-fallback events
7985a48 is described below
commit 7985a481d280b1ed32f68bbc5e90aba57bb281f0
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 19:40:31 2026 +0800
ui dashboards: strict cascade for instance/endpoint pickers + URL-fallback
events
Two related changes:
1. Instance + endpoint LIST queries now key off `serviceName`
(post-landing resolved name) instead of `serviceHandle` (URL
id fallback). The picker headers and empty-state copy also
drop the `?? serviceHandle` fallback — never show the raw
base64 ?service= id; wait for the resolved name and show a
"resolving service…" hint in the gap. Removed the now-unused
`serviceHandle` computed entirely.
Net effect: the full chain is strictly downstream now —
landing → serviceName → instance/endpoint list → metric
widgets. Each step fires exactly once after its prior
resolves, matching the trailing-control principle.
2. Auto-pick watches split "first-visit default" from "stale
URL pick fallback". First visit (no URL pick) silently
defaults. URL pick that doesn't exist in the resolved list
pushes a `fallback` info event into the debug panel
("URL instance X not in agent::gateway · falling back to Y")
so the operator sees WHY the picker landed somewhere other
than the URL asked for. Service / instance / endpoint
selectors all follow the same pattern.
---
.../render/layer-dashboard/LayerDashboardsView.vue | 117 +++++++++++++--------
1 file changed, 75 insertions(+), 42 deletions(-)
diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
index 60c8918..65de3ce 100644
--- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
@@ -38,6 +38,7 @@ import { useLayerEndpoints } from '@/layer/useLayerEndpoints';
import { useLayerInstances } from '@/layer/useLayerInstances';
import { useLayerLanding } from '@/layer/useLayerLanding';
import { useTimeRangeStore } from '@/controls/timeRange';
+import { pushEvent } from '@/controls/eventLog';
import { useLayers } from '@/shell/useLayers';
import { useSelectedEndpoint } from '@/layer/useSelectedEndpoint';
import { useSelectedInstance } from '@/layer/useSelectedInstance';
@@ -98,20 +99,6 @@ const serviceName = computed<string | null>(() => {
const match = rows.find((r) => r.serviceId === selectedId.value);
return match?.serviceName ?? null;
});
-/**
- * Service handle used for downstream picker queries (instance list,
- * endpoint list, dashboard widgets). The landing route resolves
- * `selectedId` → `serviceName` once its row sample arrives, which can
- * take a moment on first paint. The BFF picker / dashboard routes
- * accept either a name OR an id, so we fire those queries with
- * `selectedId` immediately and let `serviceName` overtake when ready.
- * Avoids the "instance list empty for a beat after landing" hiccup
- * the operator reported.
- */
-const serviceHandle = computed<string | null>(
- () => serviceName.value ?? selectedId.value ?? null,
-);
-
// Dev-only escape hatch: appending `?mockTop=10` to the page URL pads
// every TopList result to N synthetic rows. Helps operators verify
// widget heights without waiting for OAP to populate the layer.
@@ -129,9 +116,12 @@ const { config, isLoading: configLoading } =
useLayerDashboardConfig(layerKey, s
// Cleared automatically when the service changes so a stale instance
// from a previous service doesn't bleed into the next one's queries.
const { selectedInstance, setSelectedInstance } = useSelectedInstance();
+// Instance list waits for `serviceName` (post-landing), not the URL
+// id fallback. Enforces the cascade: landing → service → instance →
+// metrics, each step firing exactly once after the prior resolves.
const { instances: instanceList, isFetching: instancesLoading } =
useLayerInstances(
layerKey,
- serviceHandle,
+ serviceName,
);
/** Track which row's attributes panel is open. Mutually exclusive —
* expanding one collapses the previous so the list stays compact. */
@@ -151,7 +141,21 @@ const landingRows = computed(() =>
landing.data.value?.sampledRows ?? landing.ro
watch(landingRows, (rows) => {
const first = rows[0];
if (!first) return;
- if (!selectedId.value || !rows.some((r) => r.serviceId ===
selectedId.value)) {
+ // First-visit (no ?service= in URL) → quietly auto-pick. Stale
+ // URL pick (id present but not in the layer) → log a debug
+ // event so the operator sees the fallback in the event panel,
+ // THEN auto-pick. Distinguishes the two so the silent default
+ // isn't conflated with a fallback in the timeline.
+ if (!selectedId.value) {
+ setSelectedService(first.serviceId);
+ return;
+ }
+ if (!rows.some((r) => r.serviceId === selectedId.value)) {
+ pushEvent(
+ 'fallback',
+ 'info',
+ `URL service "${selectedId.value}" not in ${layerKey.value} · falling
back to "${first.serviceName}"`,
+ );
setSelectedService(first.serviceId);
}
}, { immediate: true });
@@ -181,7 +185,17 @@ watch([instanceList, scope], ([list, s]) => {
// the picker's own empty state handles it and the dashboard
// gate keeps the widget batch quiet.
if (list.length === 0) return;
- if (!selectedInstance.value || !list.some((i) => i.name ===
selectedInstance.value)) {
+ // Quiet default (no URL pick) vs noted fallback (stale URL pick).
+ if (!selectedInstance.value) {
+ setSelectedInstance(list[0].name);
+ return;
+ }
+ if (!list.some((i) => i.name === selectedInstance.value)) {
+ pushEvent(
+ 'fallback',
+ 'info',
+ `URL instance "${selectedInstance.value}" not in ${serviceName.value} ·
falling back to "${list[0].name}"`,
+ );
setSelectedInstance(list[0].name);
}
}, { immediate: true });
@@ -205,9 +219,11 @@ function clearEndpointSearch(): void {
endpointSearchInput.value = '';
endpointQuery.value = '';
}
+// Same cascade-strict rule as instance list — endpoint list
+// waits for `serviceName` (post-landing), not the URL handle.
const { endpoints: endpointList, isFetching: endpointsLoading } =
useLayerEndpoints(
layerKey,
- serviceHandle,
+ serviceName,
endpointQuery,
endpointLimit,
);
@@ -229,10 +245,24 @@ watchEffect(() => {
if (first) setSelectedService(first.serviceId);
return;
}
- if (selectedEndpoint.value) return;
if (endpointsLoading.value) return;
- const first = endpointList.value[0];
- if (first) setSelectedEndpoint(first.name);
+ const list = endpointList.value;
+ if (list.length === 0) return;
+ // Quiet default (no URL pick) vs noted fallback (URL endpoint not
+ // in the resolved list — log a debug event so the operator sees
+ // the fallback).
+ if (!selectedEndpoint.value) {
+ setSelectedEndpoint(list[0].name);
+ return;
+ }
+ if (!list.some((e) => e.name === selectedEndpoint.value)) {
+ pushEvent(
+ 'fallback',
+ 'info',
+ `URL endpoint "${selectedEndpoint.value}" not in ${serviceName.value} ·
falling back to "${list[0].name}"`,
+ );
+ setSelectedEndpoint(list[0].name);
+ }
});
// Drop stale endpoint when service changes.
watch(serviceName, (next, prev) => {
@@ -415,24 +445,26 @@ function isVisible(
<section v-if="scope === 'instance'" class="instance-bar sw-card">
<header class="ib-head">
<span class="kicker">Instance</span>
- <span v-if="serviceHandle" class="for-svc">
- for <b>{{ serviceName ?? serviceHandle }}</b>
+ <!-- Header strictly tracks the resolved `serviceName` —
+ never the raw `?service=` base64 id from the URL.
+ While landing is still loading we show a loading hint
+ instead, matching the cascade-clear-then-load
+ principle (downstream waits for upstream). -->
+ <span v-if="serviceName" class="for-svc">
+ for <b>{{ serviceName }}</b>
<span v-if="instanceList.length > 0" class="count">{{
instanceList.length }}</span>
</span>
- <span v-if="instancesLoading" class="hint">loading…</span>
+ <span v-else-if="selectedId" class="hint">resolving service…</span>
+ <span v-if="instancesLoading" class="hint">loading instances…</span>
</header>
- <!-- Empty-state gate keys off `serviceHandle` (URL pick OR
- resolved name) not just `serviceName`. On a hard refresh,
- landing hasn't returned yet so `serviceName` is null, but
- `serviceHandle` already has the URL service id and the
- instance query has already fired against it — so we
- render the list right away instead of flashing "Pick a
- service" while the list IS in fact loading. -->
- <div v-if="!serviceHandle" class="empty inline">
+ <div v-if="!selectedId" class="empty inline">
Pick a service in the picker above to list its instances.
</div>
+ <div v-else-if="!serviceName" class="empty inline">
+ Resolving service…
+ </div>
<div v-else-if="!instancesLoading && instanceList.length === 0"
class="empty inline">
- No active instances reported for {{ serviceName ?? serviceHandle }} in
the last 15 minutes.
+ No active instances reported for {{ serviceName }} in the last 15
minutes.
</div>
<ul v-else class="ib-list">
<li
@@ -476,20 +508,21 @@ function isVisible(
<section v-if="scope === 'endpoint'" class="instance-bar sw-card">
<header class="ib-head">
<span class="kicker">Endpoint</span>
- <span v-if="serviceHandle" class="for-svc">
- for <b>{{ serviceName ?? serviceHandle }}</b>
+ <!-- Strictly serviceName, no base64-id fallback (same rule
+ as the instance picker above). -->
+ <span v-if="serviceName" class="for-svc">
+ for <b>{{ serviceName }}</b>
<span v-if="endpointList.length > 0" class="count">{{
endpointList.length }}</span>
</span>
- <span v-if="endpointsLoading" class="hint">loading…</span>
+ <span v-else-if="selectedId" class="hint">resolving service…</span>
+ <span v-if="endpointsLoading" class="hint">loading endpoints…</span>
</header>
- <!-- Same fix as the instance picker above: gate on the URL
- service handle (id or name), not the post-landing
- resolved name, so a hard refresh doesn't flash a "Pick a
- service" empty state while the endpoint query is already
- in flight against the URL id. -->
- <div v-if="!serviceHandle" class="empty inline">
+ <div v-if="!selectedId" class="empty inline">
Pick a service in the picker above to search its endpoints.
</div>
+ <div v-else-if="!serviceName" class="empty inline">
+ Resolving service…
+ </div>
<template v-else>
<div class="ep-controls">
<input