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 7dcfaa1  ui: move Top-N pop-out trigger into the widget title bar
7dcfaa1 is described below

commit 7dcfaa1a42a810dddb0fc9415eafe064740f3b2d
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 22:13:11 2026 +0800

    ui: move Top-N pop-out trigger into the widget title bar
    
    The floating pop-out button was absolutely positioned over TopList's
    top-right corner and overlapped the in-widget tab row, blocking the tab
    switcher on multi-tab widgets (e.g. Top 20 APIs). Expose openExpanded()
    from TopList and render the trigger in the widget .w-head instead, shown
    only for top/record widgets that have data.
---
 apps/ui/src/components/charts/TopList.vue          | 39 ++------------
 .../render/layer-dashboard/LayerDashboardsView.vue | 61 +++++++++++++++++++++-
 2 files changed, 64 insertions(+), 36 deletions(-)

diff --git a/apps/ui/src/components/charts/TopList.vue 
b/apps/ui/src/components/charts/TopList.vue
index 8c6bc05..6ea0da1 100644
--- a/apps/ui/src/components/charts/TopList.vue
+++ b/apps/ui/src/components/charts/TopList.vue
@@ -103,6 +103,10 @@ function onKeydown(e: KeyboardEvent): void {
 }
 onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown));
 
+// The pop-out trigger lives in the host widget's title bar (so it can't
+// overlap the in-widget tab row); the host calls this via a template ref.
+defineExpose({ openExpanded });
+
 // Full-name hover tooltip. Row names are ellipsized to fit the widget,
 // so long api / endpoint / instance names are unreadable inline. A
 // teleported floating box (rendered to <body>, not inside the widget)
@@ -133,14 +137,6 @@ const tipStyle = computed(() => {
 
 <template>
   <div class="top-list">
-    <button
-      v-if="activeItems.length"
-      type="button"
-      class="tl-expand"
-      title="Pop out — full list"
-      aria-label="Pop out"
-      @click="openExpanded"
-    >⤢</button>
     <div v-if="showTabs" class="tabs">
       <button
         v-for="(g, i) in effectiveGroups"
@@ -227,33 +223,6 @@ const tipStyle = computed(() => {
   height: 100%;
   min-height: 0;
 }
-/* Pop-out affordance — top-right, surfaces on widget hover so it
- * doesn't clutter the dense default view. */
-.tl-expand {
-  position: absolute;
-  top: 0;
-  right: 0;
-  z-index: 2;
-  width: 20px;
-  height: 20px;
-  padding: 0;
-  font-size: 13px;
-  line-height: 1;
-  color: var(--sw-fg-3);
-  background: var(--sw-bg-1);
-  border: 1px solid var(--sw-line);
-  border-radius: 4px;
-  cursor: pointer;
-  opacity: 0;
-  transition: opacity 0.12s, color 0.12s;
-}
-.top-list:hover .tl-expand {
-  opacity: 1;
-}
-.tl-expand:hover {
-  color: var(--sw-fg-0);
-  border-color: var(--sw-line-2);
-}
 .tabs {
   display: flex;
   gap: 2px;
diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue 
b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
index ce29607..94ac133 100644
--- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue
@@ -407,6 +407,27 @@ function widgetColor(w: { id?: string; title?: string; 
expressions?: string[] })
   return colorForMetric(w.id || w.title || w.expressions?.[0] || '');
 }
 
+// Pop-out wiring for top / record widgets. The TopList renders inside the
+// widget body; its pop-out trigger lives in the widget's title bar (so it
+// can't overlap the in-widget tab row). We hold a per-widget ref to the
+// active TopList instance and call its exposed `openExpanded()`.
+type TopListExposed = { openExpanded: () => void };
+const topListRefs = new Map<string, TopListExposed>();
+function setTopListRef(id: string, el: unknown): void {
+  if (el) topListRefs.set(id, el as TopListExposed);
+  else topListRefs.delete(id);
+}
+function popOutTopList(id: string): void {
+  topListRefs.get(id)?.openExpanded();
+}
+function hasTopData(w: { id: string; type: string }): boolean {
+  const r = resultsById.value.get(w.id);
+  if (!r) return false;
+  if (w.type === 'top') return !!(r.topGroups?.length || r.topList?.length);
+  if (w.type === 'record') return !!r.records?.length;
+  return false;
+}
+
 /**
  * Evaluate a widget's `visibleWhen` predicate.
  *   - `<metric_name> has value`  → the widget's result has a non-null
@@ -655,7 +676,17 @@ function isVisible(
       >
         <div class="w-head" :title="w.tip">
           <h4>{{ w.title }}</h4>
-          <span v-if="w.unit" class="unit">{{ w.unit }}</span>
+          <div class="w-head-right">
+            <span v-if="w.unit" class="unit">{{ w.unit }}</span>
+            <button
+              v-if="(w.type === 'top' || w.type === 'record') && hasTopData(w)"
+              type="button"
+              class="w-popout"
+              title="Pop out — full list"
+              aria-label="Pop out full list"
+              @click="popOutTopList(w.id)"
+            >⤢</button>
+          </div>
         </div>
         <div class="w-body" :class="`type-${w.type}`">
           <template v-if="resultsById.get(w.id)?.error">
@@ -685,6 +716,7 @@ function isVisible(
           <template v-else-if="w.type === 'top'">
             <TopList
               v-if="resultsById.get(w.id)?.topGroups?.length"
+              :ref="(el) => setTopListRef(w.id, el)"
               :groups="resultsById.get(w.id)!.topGroups!"
               :unit="w.unit"
               :color="widgetColor(w)"
@@ -692,6 +724,7 @@ function isVisible(
             />
             <TopList
               v-else-if="resultsById.get(w.id)?.topList?.length"
+              :ref="(el) => setTopListRef(w.id, el)"
               :items="resultsById.get(w.id)!.topList!"
               :unit="w.unit"
               :color="widgetColor(w)"
@@ -705,6 +738,7 @@ function isVisible(
                  Future runtime work will add trace-id drill-in. -->
             <TopList
               v-if="resultsById.get(w.id)?.records?.length"
+              :ref="(el) => setTopListRef(w.id, el)"
               :items="resultsById.get(w.id)!.records!.map((r) => ({ name: 
r.name, value: r.value ?? null }))"
               :unit="w.unit"
               :color="widgetColor(w)"
@@ -1034,11 +1068,36 @@ function isVisible(
   overflow: hidden;
   text-overflow: ellipsis;
 }
+.w-head-right {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex: 0 0 auto;
+}
 .w-head .unit {
   font-size: 10.5px;
   color: var(--sw-fg-3);
   flex: 0 0 auto;
 }
+/* Pop-out trigger for top / record widgets — lives in the title bar so it
+ * never overlaps the in-widget tab row. */
+.w-popout {
+  width: 18px;
+  height: 18px;
+  padding: 0;
+  font-size: 12px;
+  line-height: 1;
+  color: var(--sw-fg-3);
+  background: transparent;
+  border: 1px solid var(--sw-line);
+  border-radius: 4px;
+  cursor: pointer;
+  transition: color 0.12s, border-color 0.12s;
+}
+.w-popout:hover {
+  color: var(--sw-fg-0);
+  border-color: var(--sw-line-2);
+}
 .w-body {
   /* Column-flex so charts / lists / records can flex: 1 and claim the
    * full widget height. Card-type widgets opt into centering via the

Reply via email to