This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch fix/3d-fps-and-layer-rehydrate in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 0e3892064d70ed076130514a80e92fe922f25ec0 Author: Wu Sheng <[email protected]> AuthorDate: Sun May 31 09:00:00 2026 +0800 fix(layer): honor URL ?endpoint= outside top-N; gate metrics on config #2: a deep-linked endpoint outside the recent top-N (empty-query) list was discarded; add a targeted lookup by the pinned endpoint's name and only fall back when that also finds nothing. #3: the dashboard metrics query fired before the config bundle resolved (empty widget list -> BFF defaults -> refetch). Gate it on a configReady ref so it fires once with the resolved widgets. --- .../render/layer-dashboard/LayerDashboardsView.vue | 22 +++++++++++++++++++++- .../render/layer-dashboard/useLayerDashboard.ts | 10 ++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue index dedc6f3..7ef58ea 100644 --- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue +++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue @@ -264,6 +264,18 @@ const { endpoints: endpointList, isFetching: endpointsLoading } = useLayerEndpoi endpointQuery, endpointLimit, ); +// URL-pinned endpoint validation. The list above is the recent top-N +// (empty query); a deep-linked endpoint outside it would look "stale". +// This re-queries by the pinned endpoint's own name to confirm it really +// exists for this service before we discard the deep link. Inactive +// (empty query) once the pin is null or already present in the default list. +const pinnedEndpointQuery = computed(() => { + const pinned = selectedEndpoint.value; + if (!pinned) return ''; + return endpointList.value.some((e) => e.name === pinned) ? '' : pinned; +}); +const { endpoints: pinnedEndpointMatches, isFetching: pinnedEndpointLoading } = + useLayerEndpoints(layerKey, serviceName, pinnedEndpointQuery, endpointLimit); // Endpoint-scope orchestration — explicit sequence so the loading // flow is deterministic: // 1. wait for landing rows @@ -293,10 +305,14 @@ watchEffect(() => { return; } if (!list.some((e) => e.name === selectedEndpoint.value)) { + // Outside the default top-N — confirm via the targeted name search + // before discarding the deep link. + if (pinnedEndpointQuery.value && pinnedEndpointLoading.value) return; // wait for the lookup + if (pinnedEndpointMatches.value.some((e) => e.name === selectedEndpoint.value)) return; // valid → keep pushEvent( 'fallback', 'info', - `URL endpoint "${selectedEndpoint.value}" not in ${serviceName.value} · falling back to "${list[0].name}"`, + `URL endpoint "${selectedEndpoint.value}" not found in ${serviceName.value} · falling back to "${list[0].name}"`, ); setSelectedEndpoint(list[0].name); } @@ -334,6 +350,9 @@ const effectiveEndpoint = computed<string | null>(() => { return endpointList.value.some((e) => e.name === v) ? v : null; }); const widgetsForQuery = computed(() => config.value?.widgets ?? []); +// Hold the metrics fetch until the dashboard config bundle has resolved, +// so the widget list fires once (resolved) rather than empty-then-refetch. +const configReady = computed(() => config.value !== null); const { data, isFetching, error } = useLayerDashboard( layerKey, serviceName, @@ -342,6 +361,7 @@ const { data, isFetching, error } = useLayerDashboard( { instance: effectiveInstance, endpoint: effectiveEndpoint }, rangeRef, widgetsForQuery, + configReady, ); // Sequential page-init events for the EventTicker — config → diff --git a/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts b/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts index e70fe72..7b86a2b 100644 --- a/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts +++ b/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts @@ -110,6 +110,13 @@ export function useLayerDashboard( * back to a single BFF call that resolves widgets server-side * (used by callers that don't have the config bundle handy). */ widgetsList?: Ref<DashboardWidget[]>, + /** Optional config-bundle readiness gate. When supplied, the metrics + * query waits until it is true, so the dashboard fires ONCE with the + * resolved widget list instead of firing first with an empty list + * (which makes the BFF substitute defaults) and refetching when the + * bundle lands. Callers without a config bundle omit it (treated as + * ready) and keep the server-resolves-widgets behaviour. */ + configReady?: Ref<boolean>, ) { // Auto-refresh is metrics-only. Trace / log / profiling pages are // explore-style (operator-driven queries, log tails, etc.) and would @@ -194,6 +201,9 @@ export function useLayerDashboard( // - endpoint scope needs service + endpoint. enabled: computed(() => { if (layerKey.value.length === 0) return false; + // Wait for the config bundle so widgets are resolved before the + // metrics fire (no empty-list → BFF-default → refetch round-trip). + if (configReady && !configReady.value) return false; const s = scope?.value ?? 'service'; if (s === 'service') return Boolean(service.value); if (s === 'instance') return Boolean(service.value && entityRefs.instance?.value);
