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 cc631e1  ui dashboards: ticking "x/y metrics firing" via UI-side 
widget chunking
cc631e1 is described below

commit cc631e16dfce0eb17468786202ee95e43c9dd6df
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 20:07:23 2026 +0800

    ui dashboards: ticking "x/y metrics firing" via UI-side widget chunking
    
    useLayerDashboard now accepts the widget list (LayerDashboardsView
    already has it from the bundled config). When supplied, the SPA
    chunks widgets into groups of 6 (booster-ui's
    DashboardMaxQueryWidgets) and fires N parallel BFF calls instead
    of the single all-widgets request — exposes a per-chunk
    `progress: { arrived, total }` ref that ticks up as each chunk's
    BFF call resolves.
    
    The loading indicator now reads, live:
    
      Reading data for agent::gateway / a0da… · 0/12 metrics firing
      Reading data for agent::gateway / a0da… · 6/12 metrics firing
      Reading data for agent::gateway / a0da… · 12/12 metrics ready
    
    Wall-clock stays close to a single round-trip (Promise.all
    dispatches the chunks concurrently); each chunk's GraphQL payload
    is small enough to stay inside OAP's per-query budget without
    relying on BFF-internal chunking. First-arrived chunk donates the
    response envelope (`service`, `reachable`, …) and subsequent
    chunks contribute only widget results, merged in order.
    
    Legacy single-call path (no widgetsList passed) is preserved for
    callers that don't have the config bundle handy — BFF still
    resolves widgets server-side in that case.
---
 .../render/layer-dashboard/LayerDashboardsView.vue |   9 +-
 .../render/layer-dashboard/useLayerDashboard.ts    | 112 +++++++++++++++++----
 2 files changed, 99 insertions(+), 22 deletions(-)

diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue 
b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
index f603254..cccfd84 100644
--- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
@@ -303,14 +303,17 @@ const effectiveEndpoint = computed<string | null>(() => {
   if (!v) return null;
   return endpointList.value.some((e) => e.name === v) ? v : null;
 });
-const { data, isFetching, error } = useLayerDashboard(
+const widgetsForQuery = computed(() => config.value?.widgets ?? []);
+const { data, isFetching, error, progress } = useLayerDashboard(
   layerKey,
   serviceName,
   scope,
   mockTop,
   { instance: effectiveInstance, endpoint: effectiveEndpoint },
   rangeRef,
+  widgetsForQuery,
 );
+const widgetsArrived = computed(() => progress.value.arrived);
 
 // Sequential page-init events for the EventTicker — config →
 // services → service → instances/endpoints → instance/endpoint →
@@ -650,7 +653,9 @@ function isVisible(
             / <b>{{ selectedEndpoint }}</b>
           </template>
         </template>
-        …
+        <span class="reading-progress">
+          · {{ widgetsArrived }}/{{ widgets.length }} metric{{ widgets.length 
=== 1 ? '' : 's' }} {{ widgetsArrived === widgets.length ? 'ready' : 'firing' }}
+        </span>
       </span>
     </div>
     <div v-else class="grid">
diff --git a/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts 
b/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts
index 46620a4..7748236 100644
--- a/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts
+++ b/apps/ui/src/render/layer-dashboard/useLayerDashboard.ts
@@ -27,7 +27,7 @@
  * viewed service is instant.
  */
 
-import { computed, type Ref } from 'vue';
+import { computed, ref, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
 import { useAutoRefreshSubscribe } from 
'../../controls/useAutoRefreshSubscribe';
 import { bffClient } from '@/api/client';
@@ -36,7 +36,12 @@ import {
   getDashboardConfig,
   useConfigBundle,
 } from '@/controls/configBundle';
-import type { DashboardConfig } from '@skywalking-horizon-ui/api-client';
+import type {
+  DashboardConfig,
+  DashboardResponse,
+  DashboardWidget,
+  DashboardWidgetResult,
+} from '@skywalking-horizon-ui/api-client';
 
 export function useLayerDashboardConfig(layerKey: Ref<string>, scope?: 
Ref<string>) {
   // Prefer the preloaded bundle. The bundle preload kicks off at app
@@ -99,6 +104,14 @@ export function useLayerDashboard(
    *  time-picker change refires the widget batch the same way a
    *  service/instance pick does. */
   range?: Ref<DashboardRange | null>,
+  /** Optional widget list. When supplied the SPA chunks it into
+   *  groups (matching booster-ui's DashboardMaxQueryWidgets = 6)
+   *  and fires N parallel BFF calls instead of one — exposes the
+   *  per-chunk completion as `widgetsArrived` so the loading
+   *  indicator can tick "x fired / y all". When omitted, falls
+   *  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[]>,
 ) {
   // Auto-refresh is metrics-only. Trace / log / profiling pages are
   // explore-style (operator-driven queries, log tails, etc.) and would
@@ -121,6 +134,13 @@ export function useLayerDashboard(
     if (!r) return null;
     return `${r.step}:${Math.floor(r.startMs / 60_000)}:${Math.floor(r.endMs / 
60_000)}`;
   });
+  // Per-chunk progress for the loading indicator. Resets to
+  // {arrived: 0, total: N} at queryFn start, ticks up as each
+  // parallel BFF chunk resolves. The widget-list path uses this;
+  // the legacy single-call path leaves it at {0, 0}.
+  const progress = ref<{ arrived: number; total: number }>({ arrived: 0, 
total: 0 });
+  const CHUNK_SIZE = 6;
+
   const q = useQuery({
     queryKey: [
       'dashboard',
@@ -131,25 +151,72 @@ export function useLayerDashboard(
       entityRefs.instance ?? computed(() => null),
       entityRefs.endpoint ?? computed(() => null),
       rangeKey,
+      // Hash of widget ids so adding/removing a widget refires.
+      computed(() => widgetsList?.value.map((w) => w.id).join('|') ?? null),
     ],
-    queryFn: () =>
-      bffClient.layer.dashboard(
-        layerKey.value,
-        {
-          ...(service.value ? { service: service.value } : {}),
-          ...(scope?.value ? { scope: scope.value } : {}),
-          ...(entityRefs.instance?.value ? { serviceInstance: 
entityRefs.instance.value } : {}),
-          ...(entityRefs.endpoint?.value ? { endpoint: 
entityRefs.endpoint.value } : {}),
-          ...(rangeRef.value
-            ? {
-                step: rangeRef.value.step,
-                startMs: rangeRef.value.startMs,
-                endMs: rangeRef.value.endMs,
-              }
-            : {}),
-        },
-        mockTop?.value ? { mockTop: mockTop.value } : {},
-      ),
+    queryFn: async () => {
+      const baseBody = {
+        ...(service.value ? { service: service.value } : {}),
+        ...(scope?.value ? { scope: scope.value } : {}),
+        ...(entityRefs.instance?.value ? { serviceInstance: 
entityRefs.instance.value } : {}),
+        ...(entityRefs.endpoint?.value ? { endpoint: entityRefs.endpoint.value 
} : {}),
+        ...(rangeRef.value
+          ? {
+              step: rangeRef.value.step,
+              startMs: rangeRef.value.startMs,
+              endMs: rangeRef.value.endMs,
+            }
+          : {}),
+      };
+      const opts = mockTop?.value ? { mockTop: mockTop.value } : {};
+      const all = widgetsList?.value ?? [];
+      // No widget list → fall back to the legacy single-call path:
+      // BFF resolves widgets from the layer template + auto-picks
+      // entities. No per-chunk progress (no list to chunk against).
+      if (all.length === 0) {
+        progress.value = { arrived: 0, total: 0 };
+        return bffClient.layer.dashboard(layerKey.value, baseBody, opts);
+      }
+      // Chunked path. Fire N parallel BFF calls (one per 6-widget
+      // group) so each chunk's completion ticks the progress ref.
+      // Wall-clock is roughly max(chunk_time) thanks to Promise.all;
+      // each chunk is small enough to stay inside OAP's per-query
+      // budget without relying on BFF-internal chunking.
+      progress.value = { arrived: 0, total: all.length };
+      const chunks: DashboardWidget[][] = [];
+      for (let i = 0; i < all.length; i += CHUNK_SIZE) {
+        chunks.push(all.slice(i, i + CHUNK_SIZE));
+      }
+      const merged: DashboardWidgetResult[] = [];
+      let baseResp: DashboardResponse | null = null;
+      await Promise.all(
+        chunks.map(async (chunk) => {
+          const resp = await bffClient.layer.dashboard(
+            layerKey.value,
+            { ...baseBody, widgets: chunk },
+            opts,
+          );
+          // First-arrived chunk donates the response envelope
+          // (`service`, `reachable`, `durationStart/End`, etc.) —
+          // all chunks share the same upstream state since they
+          // were resolved against the same entity.
+          if (!baseResp) baseResp = resp;
+          merged.push(...(resp.widgets ?? []));
+          progress.value = { arrived: merged.length, total: all.length };
+        }),
+      );
+      const finalEnvelope = baseResp ?? {
+        layer: layerKey.value,
+        service: service.value ?? null,
+        generatedAt: Date.now(),
+        step: 'MINUTE' as const,
+        durationStart: '',
+        durationEnd: '',
+        reachable: true,
+        widgets: [],
+      };
+      return { ...finalEnvelope, widgets: merged };
+    },
     // Trailing-control principle: the widget batch is the deepest
     // control in the chain and must wait for everything upstream
     // (layer → service → instance/endpoint) to be resolved by the
@@ -185,5 +252,10 @@ export function useLayerDashboard(
     isFetching: q.isFetching,
     error: q.error,
     refetch: q.refetch,
+    /** Per-chunk progress for the loading indicator. While a fetch
+     *  is in flight, `arrived` ticks from 0 → total as each parallel
+     *  chunk's BFF call resolves. `total` is 0 when the legacy
+     *  no-widgetsList path is in use. */
+    progress,
   };
 }

Reply via email to