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" />

Reply via email to