This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch fix/layer-aliases-and-empty-entity-hang in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit ef5e4f9ecff66e23c07080a0a75cd46f79ca3d3e Author: Wu Sheng <[email protected]> AuthorDate: Wed Jun 3 16:25:05 2026 +0800 fix(layer): apply scope aliases and stop empty-entity dashboard hang Per-layer Instance/Endpoint section headers and the service-picker column header now resolve the layer's slot aliases (Brokers, Topics, Destinations, "ActiveMQ clusters", …) instead of the hardcoded scope words, matching the sidebar nav. Instance/Endpoint dashboard scopes gate the widget batch on a resolved entity; when a service reports no instances/endpoints the query never fired and the page spun "Reading data…" forever. Detect that terminal empty state, suppress the overlay, and fall through to the widget grid so every widget renders in its empty "no data" state. --- CHANGELOG.md | 14 +++++++++ apps/ui/src/layer/LayerServiceSelector.vue | 6 +++- apps/ui/src/layer/LayerShell.vue | 4 ++- .../render/layer-dashboard/LayerDashboardsView.vue | 33 ++++++++++++++++++++-- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851d697..5a82aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,20 @@ packages) plus the BFF's `HORIZON_VERSION` default. the post-vote finalize script now only verifies the published tags (the manual local-push fallback and Docker Hub login preflight were removed). +### Layer drill-down fixes + +- The per-layer **Instance** and **Endpoint** pages now honor the layer's + configured aliases in their section headers and in the service-picker's + name column — e.g. ActiveMQ reads **Brokers** / **Destinations** and + Virtual MQ reads **Topics** / **MQ clusters**, matching the sidebar — + instead of the generic "Instance" / "Endpoint" / "Service" labels. Layers + that define no alias still read the generic words. +- A layer's Instance or Endpoint page no longer hangs on a perpetual + "Reading data…" when the selected service reports no instances or + endpoints (or a search matches nothing). It now shows the empty picker and + renders the metric widgets in their normal "no data" state, so the layout + stays visible and ready for services that do report them. + ## 0.6.0 This release is the production-readiness pass for Horizon UI: every page diff --git a/apps/ui/src/layer/LayerServiceSelector.vue b/apps/ui/src/layer/LayerServiceSelector.vue index 207824a..8cb63aa 100644 --- a/apps/ui/src/layer/LayerServiceSelector.vue +++ b/apps/ui/src/layer/LayerServiceSelector.vue @@ -47,6 +47,10 @@ const props = withDefaults( * selector — it's the global service picker, not the topology * view, so per-topology `showGroup` doesn't apply here. */ namingRule?: ServiceNamingRule | null; + /** Layer's service-slot alias for the name column header (e.g. + * "ActiveMQ clusters", "Databases"). Falls back to the generic + * "Service" when the layer defines no alias. */ + serviceLabel?: string; }>(), { accent: 'var(--sw-accent)', @@ -95,7 +99,7 @@ function colorForStatus(s: 'ok' | 'warn' | 'err'): string { <table class="sw-table picker-table"> <thead> <tr> - <th class="svc-col">{{ t('Service') }}</th> + <th class="svc-col">{{ serviceLabel || t('Service') }}</th> <th v-for="c in columns" :key="c.metric" diff --git a/apps/ui/src/layer/LayerShell.vue b/apps/ui/src/layer/LayerShell.vue index e0b2438..fbf4e3c 100644 --- a/apps/ui/src/layer/LayerShell.vue +++ b/apps/ui/src/layer/LayerShell.vue @@ -392,6 +392,7 @@ function initialsFor(name: string): string { } const displayName = computed(() => cfg.value?.displayName || layer.value?.name || layerKey.value); const initials = computed(() => initialsFor(displayName.value)); +const serviceSlotLabel = computed(() => layer.value?.slots.services || 'services'); // ── Tabs ───────────────────────────────────────────────────────────── // ── Header KPI strip ───────────────────────────────────────────────── @@ -491,7 +492,7 @@ const serviceKpis = computed<HeaderKpi[]>(() => { <span v-else-if="!layer.active" class="sw-badge">no data</span> </div> <div class="sub"> - {{ layer.serviceCount >= 0 ? `${layer.serviceCount} ${(cfg?.slots.services || 'services').toLowerCase()}` : 'no service data' }} + {{ layer.serviceCount >= 0 ? `${layer.serviceCount} ${serviceSlotLabel.toLowerCase()}` : 'no service data' }} <span v-if="layer.documentLink">· <a :href="layer.documentLink" target="_blank" rel="noopener noreferrer">docs ↗</a> </span> @@ -590,6 +591,7 @@ const serviceKpis = computed<HeaderKpi[]>(() => { :selected-id="selectedId" :accent="layer.color" :naming-rule="layer.naming ?? null" + :service-label="layer.slots.services" @select="pickService" /> diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue index 3a49d8f..7a6b4b1 100644 --- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue +++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue @@ -354,6 +354,33 @@ const effectiveEndpoint = computed<string | null>(() => { if (pinnedEndpointMatches.value.some((e) => e.name === v)) return v; return null; }); +// Instance / endpoint scopes gate the widget batch on a resolved entity +// (see useLayerDashboard `enabled`). When the entity list settles EMPTY — +// the service genuinely reports no instances / endpoints (or a search +// matched nothing) — no entity can ever be picked, the dashboard query +// never fires, and `dataIsFresh` never flips. Used only to suppress the +// reset-then-load overlay in that terminal state so it doesn't spin +// "Reading data…" forever; the page then falls through to the widget +// grid, which renders every widget in its empty "no data" state (the +// layout still reads — another MQ / service may well have topics). +// `resolvable` stays true while a list / pin lookup is still in flight so +// the overlay still covers the genuine wait. +const endpointResolvable = computed<boolean>( + () => + endpointsLoading.value || + pinnedEndpointLoading.value || + endpointList.value.length > 0 || + !!effectiveEndpoint.value, +); +const instanceResolvable = computed<boolean>( + () => instancesLoading.value || instanceList.value.length > 0 || !!effectiveInstance.value, +); +const noEntityToChart = computed<boolean>(() => { + if (!serviceName.value) return false; // service still resolving — keep waiting + if (scope.value === 'instance') return !instanceResolvable.value; + if (scope.value === 'endpoint') return !endpointResolvable.value; + return false; +}); const widgetsForQuery = computed(() => config.value?.widgets ?? []); // Hold the metrics fetch until the config bundle has resolved WITH widgets. // A resolved-but-empty config means "no dashboard for this layer/scope", @@ -554,7 +581,7 @@ function isVisible( so a stale instance can't sneak into queries. --> <section v-if="scope === 'instance'" class="instance-bar sw-card"> <header class="ib-head"> - <span class="kicker">Instance</span> + <span class="kicker">{{ safeLayer.slots.instances || 'Instance' }}</span> <!-- Header strictly tracks the resolved `serviceName` — never the raw `?service=` base64 id from the URL. While landing is still loading we show a loading hint @@ -617,7 +644,7 @@ function isVisible( the limit to 20…50. --> <section v-if="scope === 'endpoint'" class="instance-bar sw-card"> <header class="ib-head"> - <span class="kicker">Endpoint</span> + <span class="kicker">{{ safeLayer.slots.endpoints || 'Endpoint' }}</span> <!-- Strictly serviceName, no base64-id fallback (same rule as the instance picker above). --> <span v-if="serviceName" class="for-svc"> @@ -705,7 +732,7 @@ function isVisible( branch below only shows once config has actually loaded and the layer genuinely defines none. --> <div - v-if="reachable && (configLoading || (!dataIsFresh && widgets.length > 0))" + v-if="reachable && !noEntityToChart && (configLoading || (!dataIsFresh && widgets.length > 0))" class="empty reading" > <span class="reading-dot" />
