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 fe82fd4  ui: service list fetched once + manual refresh + page-level 
"Reading data…"
fe82fd4 is described below

commit fe82fd49d8b10ed541b9e21497cc394eb62be7bc
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 17:55:57 2026 +0800

    ui: service list fetched once + manual refresh + page-level "Reading data…"
    
    Three operator-driven changes:
    
    1. Service list (useLayerLanding) is fetched once per (layer, cfg,
       range) and stays cached. Removed `refetchInterval`,
       `refetchOnWindowFocus`, and the global auto-refresh
       subscription. `staleTime: Infinity` so vue-query never
       considers the data stale on its own.
    
    2. Manual refresh button next to the layer header's service
       Switch. Spinning ↻ icon while in flight; disabled during the
       fetch to debounce double-clicks. Tooltip flips between
       "Refresh service list" / "Refreshing service list…".
    
    3. Page-level "Reading data…" indicator replaces the per-widget
       "loading…" cells while the dashboard batch is in flight without
       any prior result. The widget batch is server-side batched —
       all widgets land together, so one indicator is cleaner than N
       independent cards rendering the same waiting state. Subsequent
       refetches (auto-refresh ticker, manual) keep the prior data
       visible during the fetch, no flash.
---
 apps/ui/src/layer/LayerShell.vue                   | 28 +++++++++++++++++++++
 apps/ui/src/layer/useLayerLanding.ts               | 15 ++++++-----
 .../render/layer-dashboard/LayerDashboardsView.vue | 29 ++++++++++++++++++++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/apps/ui/src/layer/LayerShell.vue b/apps/ui/src/layer/LayerShell.vue
index 8475e6c..eff3ea0 100644
--- a/apps/ui/src/layer/LayerShell.vue
+++ b/apps/ui/src/layer/LayerShell.vue
@@ -348,6 +348,19 @@ const serviceKpis = computed<HeaderKpi[]>(() => {
           <span v-if="selectedGroup" class="svc-group">{{ selectedGroup 
}}</span>
           <span class="svc-name">{{ selectedName }}</span>
         </button>
+        <!-- Manual refresh — the service list is fetched once per
+             (layer, cfg, range) and stays cached afterwards, so the
+             operator needs an explicit affordance to pull a fresh
+             list. Spins while the refetch is in flight. -->
+        <button
+          class="sw-btn ghost svc-refresh"
+          type="button"
+          :title="landing.isFetching.value ? 'Refreshing service list…' : 
'Refresh service list'"
+          :disabled="landing.isFetching.value"
+          @click="() => landing.refetch()"
+        >
+          <Icon name="refresh" :class="{ spin: landing.isFetching.value }" />
+        </button>
         <div v-if="serviceKpis.length > 0" class="kpi-strip service-kpis">
           <div v-for="(k, i) in serviceKpis" :key="i" class="kpi compact">
             <span class="kpi-label inline">
@@ -411,6 +424,21 @@ const serviceKpis = computed<HeaderKpi[]>(() => {
 </template>
 
 <style scoped>
+.svc-refresh {
+  width: 32px;
+  height: 32px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+}
+.svc-refresh :deep(svg.spin) {
+  animation: svc-refresh-spin 0.9s linear infinite;
+}
+@keyframes svc-refresh-spin {
+  to { transform: rotate(360deg); }
+}
+
 .layer-shell {
   padding: 16px 20px 48px;
   max-width: 1440px;
diff --git a/apps/ui/src/layer/useLayerLanding.ts 
b/apps/ui/src/layer/useLayerLanding.ts
index e0ae23f..52bfa1a 100644
--- a/apps/ui/src/layer/useLayerLanding.ts
+++ b/apps/ui/src/layer/useLayerLanding.ts
@@ -17,7 +17,6 @@
 
 import { computed, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
-import { useAutoRefreshSubscribe } from '../controls/useAutoRefreshSubscribe';
 import type { LandingConfig, LandingResponse, LayerDef } from 
'@skywalking-horizon-ui/api-client';
 import { bffClient } from '@/api/client';
 
@@ -62,6 +61,13 @@ export function useLayerLanding(
     return `${r.step}:${Math.floor(r.startMs / 60_000)}:${Math.floor(r.endMs / 
60_000)}`;
   });
 
+  // Service list is fetched ONCE per (layer, cfg, range) and stays
+  // cached. No `refetchInterval`, no `refetchOnWindowFocus`, not
+  // wired into the global auto-refresh ticker: the operator-facing
+  // contract is "service list doesn't move under you". Manual
+  // refresh is the `q.refetch()` handle exposed below — wired to
+  // a refresh button in LayerShell so operators can pull a fresh
+  // list on demand.
   const q = useQuery({
     queryKey: ['layer-landing', layerKey, cfgHash, rangeKey],
     queryFn: () =>
@@ -70,14 +76,11 @@ export function useLayerLanding(
         cfg.value,
         rangeRef.value ?? undefined,
       ),
-    staleTime: 45_000,
-    refetchInterval: 60_000,
-    refetchOnWindowFocus: true,
+    staleTime: Infinity,
+    refetchOnWindowFocus: false,
     retry: 1,
   });
 
-  useAutoRefreshSubscribe(() => q.refetch());
-
   const data = computed<LandingResponse | null>(() => q.data.value ?? null);
   const rows = computed(() => data.value?.rows ?? []);
   const reachable = computed(() => data.value?.reachable ?? false);
diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue 
b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
index eca4d87..fc6babb 100644
--- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
@@ -537,6 +537,17 @@ function isVisible(
     <div v-else-if="widgets.length === 0" class="empty">
       No widgets defined for this layer. Phase 7 admin will let operators add 
their own.
     </div>
+    <!-- Single page-level loading state while the widget batch is
+         in flight without a prior result. Replaces the per-widget
+         "loading…" cards from f9866cf — the dashboard query is
+         batched so the widgets all land together; one indicator is
+         cleaner than N. Once the result is in `data`, the grid
+         takes over and shows each widget's value / no-data / error
+         normally. Background refetches (auto-refresh) keep showing
+         the prior data, no flash. -->
+    <div v-else-if="isFetching && data === null" class="empty reading">
+      <span class="reading-dot" /> Reading data…
+    </div>
     <div v-else class="grid">
       <div
         v-for="w in widgets.filter((wi) => isVisible(wi, 
resultsById.get(wi.id)))"
@@ -651,6 +662,24 @@ function isVisible(
   color: var(--sw-fg-3);
   font-size: 12px;
 }
+.empty.reading {
+  color: var(--sw-fg-1);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+}
+.reading-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--sw-accent);
+  animation: reading-pulse 1.1s ease-in-out infinite;
+}
+@keyframes reading-pulse {
+  0%, 100% { opacity: 0.25; transform: scale(0.7); }
+  50%      { opacity: 1;    transform: scale(1);   }
+}
 .instance-bar {
   padding: 0;
   display: flex;

Reply via email to