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 cfdf93d  bff dashboard: auto-pick first instance / endpoint when scope 
needs one
cfdf93d is described below

commit cfdf93d5a88b89975df03bd9a23525d411c34dbe
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 10:50:42 2026 +0800

    bff dashboard: auto-pick first instance / endpoint when scope needs one
    
    When the SPA hits POST /api/layer/<key>/dashboard with scope=instance
    (or scope=endpoint) but no serviceInstance / endpoint body field, the
    BFF now probes OAP for the service's first instance / endpoint and
    fills it in before building the MQE fragments.
    
    Without this, the first paint of /layer/<key>/instance for instance-
    only layers (so11y_*, mesh_dp, jvm-heavy layers) sees the dashboard
    fire with a Service-scope entity against ServiceInstance-scope
    metrics — every widget renders "no data" until the UI's separate
    useLayerInstances composable resolves and the dashboard query re-
    fires with the picked instance. Symmetric pattern already existed
    for service auto-pick at the top of the handler; extends it to
    instance + endpoint.
    
    Verified on demo OAP:
      - so11y_java_agent / instance:  7 widgets all return real series
      - mesh / endpoint:               5 widgets all return real series
---
 apps/bff/src/http/query/dashboard.ts | 65 ++++++++++++++++++++++++++++++++++--
 1 file changed, 63 insertions(+), 2 deletions(-)

diff --git a/apps/bff/src/http/query/dashboard.ts 
b/apps/bff/src/http/query/dashboard.ts
index fa009ed..92c518b 100644
--- a/apps/bff/src/http/query/dashboard.ts
+++ b/apps/bff/src/http/query/dashboard.ts
@@ -146,6 +146,27 @@ const LIST_FIRST_SERVICE = /* GraphQL */ `
   }
 `;
 
+/** Auto-pick a default instance/endpoint when the caller asks for the
+ *  matching scope but doesn't carry an explicit `serviceInstance` /
+ *  `endpoint` body field. Without this the dashboard fires with a
+ *  Service-scope entity and every ServiceInstance / Endpoint metric
+ *  (so11y_* meters, envoy_cluster_*, JVM metrics, endpoint_cpm, …)
+ *  returns "no data" on first paint. */
+const LIST_FIRST_INSTANCE = /* GraphQL */ `
+  query FirstInstance($serviceId: ID!, $duration: Duration!) {
+    instances: listInstances(serviceId: $serviceId, duration: $duration) {
+      id name
+    }
+  }
+`;
+const FIND_FIRST_ENDPOINT = /* GraphQL */ `
+  query FirstEndpoint($serviceId: ID!, $duration: Duration!) {
+    endpoints: findEndpoint(serviceId: $serviceId, keyword: "", limit: 1, 
duration: $duration) {
+      id name
+    }
+  }
+`;
+
 const DEFAULT_WINDOW_MIN = 60;
 
 export interface Window {
@@ -342,6 +363,7 @@ export function registerDashboardQueryRoute(app: 
FastifyInstance, deps: Dashboar
         parsed.data.widgets ??
         (tpl ? widgetsForScope(tpl, scope) : defaultWidgetsFor(layerKey));
       let serviceName = parsed.data.service ?? '';
+      let serviceId = '';
       let normal = true;
       const cfgCurrent = deps.config.current;
       const opts = buildOapOpts(cfgCurrent, deps.fetch);
@@ -391,6 +413,7 @@ export function registerDashboardQueryRoute(app: 
FastifyInstance, deps: Dashboar
           }
         }
         serviceName = picked.name;
+        serviceId = picked.id;
         normal = picked.normal !== false;
         baseResp.service = serviceName;
       } catch (err) {
@@ -402,6 +425,46 @@ export function registerDashboardQueryRoute(app: 
FastifyInstance, deps: Dashboar
         });
       }
 
+      // Step 1b — auto-pick instance/endpoint when scope requires one
+      // but the caller didn't pass one. Without this, the first paint
+      // on /instance or /endpoint fires Service-scope queries against
+      // ServiceInstance / Endpoint-scope metrics and every widget
+      // shows "no data" until the UI's instance/endpoint picker
+      // resolves and the dashboard re-fires. Symmetric to the
+      // listServices auto-pick above.
+      let selectedInstance: string | null = parsed.data.serviceInstance ?? 
null;
+      let selectedEndpoint: string | null = parsed.data.endpoint ?? null;
+      if (scope === 'instance' && !selectedInstance && serviceId) {
+        try {
+          const data = await graphqlPost<{ instances: Array<{ id: string; 
name: string }> }>(
+            opts,
+            LIST_FIRST_INSTANCE,
+            {
+              serviceId,
+              duration: { start: window.start, end: window.end, step: 'MINUTE' 
},
+            },
+          );
+          selectedInstance = data.instances?.[0]?.name ?? null;
+        } catch {
+          /* leave selectedInstance null — widgets surface "no data" */
+        }
+      }
+      if (scope === 'endpoint' && !selectedEndpoint && serviceId) {
+        try {
+          const data = await graphqlPost<{ endpoints: Array<{ id: string; 
name: string }> }>(
+            opts,
+            FIND_FIRST_ENDPOINT,
+            {
+              serviceId,
+              duration: { start: window.start, end: window.end, step: 'MINUTE' 
},
+            },
+          );
+          selectedEndpoint = data.endpoints?.[0]?.name ?? null;
+        } catch {
+          /* leave selectedEndpoint null — widgets surface "no data" */
+        }
+      }
+
       // Step 2 — batch all widget × expression queries into one GraphQL trip.
       const fragments: string[] = [];
       const aliasMap = new Map<string, { wIdx: number; eIdx: number }>();
@@ -411,8 +474,6 @@ export function registerDashboardQueryRoute(app: 
FastifyInstance, deps: Dashboar
       // both (they ignore the selected entity by design — they're
       // layer-wide rollups). When neither override applies, we keep
       // the legacy Service-scope behavior.
-      const selectedInstance = parsed.data.serviceInstance ?? null;
-      const selectedEndpoint = parsed.data.endpoint ?? null;
       const scopeHonorsInstance = scope === 'instance';
       const scopeHonorsEndpoint = scope === 'endpoint';
       widgets.forEach((widget, wIdx) => {

Reply via email to