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