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;