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);

Reply via email to