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 15e0360 fix(layer-dashboards): keep the widget editor's move / delete
controls in view (#75)
15e0360 is described below
commit 15e03601603fef52bc9b64fc3db86ad600d1f642
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Thu Jun 25 09:20:45 2026 +0800
fix(layer-dashboards): keep the widget editor's move / delete controls in
view (#75)
The per-widget editor drawer kept Up / Down / Delete as the last block
inside a
scrolling body, and its fixed height (calc(100vh - 52px)) overshot because
the
drawer sits below the editor card's header — so the drawer's bottom, with
the
Delete button, fell below the viewport, hidden behind a scrollbar even on a
short board with few widgets.
Move Up / Down / Delete into a pinned footer outside the scroll region, and
size
the drawer in JS from its live top offset down to the viewport bottom
(re-synced
on open, scroll, and resize) so the footer is always visible regardless of
pin
state or board height. Also scroll a newly-added widget into view, next to
the
editor that opens for it, instead of leaving it off-screen at the canvas
bottom.
---
CHANGELOG.md | 4 ++
.../admin/layer-templates/LayerDashboardsAdmin.vue | 53 +++++++++++++++++++---
2 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d547cb..cb9234e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,10 @@ The version line is shared by every package in the monorepo
(apps + shared packa
- **Single-value metrics now render as cards, not flat lines, on several layer
dashboards.** Widgets whose expression collapses the window to one number (a
`latest(...)` total) had been mis-ported as line charts — drawn as a lone dot
that misreads as a time series and shares one axis with an unrelated average
trend. Each is now split into a proper single-value **card** (the total) plus a
trend **line** (the average), matching the metric's shape, the way booster-ui
rendered them. Affects [...]
+### Layer dashboard editor
+
+- **The widget editor's move / delete controls stay reachable.** On the Layer
dashboards admin, the per-widget editor's `Up` / `Down` / `Delete` row is now a
pinned footer instead of the last thing in a scrolling panel, and the panel
sizes itself to the visible area — so Delete no longer slips below the fold on
a long form or a short board. Adding a widget also scrolls the new widget into
view, next to the editor that opens for it, instead of leaving it off-screen at
the bottom of the canvas.
+
## 0.7.0
### Browser errors & source maps
diff --git
a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index e3ac142..52288fe 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -600,9 +600,29 @@ watch(
// without resizing the nav, so refresh on those too.
watch([visibleScopes, activeScope], () => void nextTick(updateScopeScroll));
function onWinResizeScope(): void { updateScopeScroll(); }
-onMounted(() => window.addEventListener('resize', onWinResizeScope));
+
+// ── Editor drawer height. The drawer is a sticky sidebar that sits below the
+// card header, so a fixed `100vh - 52px` overshoots (the header offsets the
+// drawer's top) and pushes its pinned footer — Up / Down / Delete — below the
+// fold; worst on a short board you can't scroll far enough to pin. Size it to
+// the live space from wherever it currently sits down to the viewport bottom
+// instead, so the footer is always visible. Scroll listens in the capture
+// phase so the inner content pane's scroll (not just window) re-syncs it.
+const drawerEl = ref<HTMLElement | null>(null);
+function syncDrawerHeight(): void {
+ const el = drawerEl.value;
+ if (!el) return;
+ el.style.height = `${Math.max(220, window.innerHeight -
el.getBoundingClientRect().top - 8)}px`;
+}
+onMounted(() => {
+ window.addEventListener('resize', onWinResizeScope);
+ window.addEventListener('scroll', syncDrawerHeight, { capture: true,
passive: true });
+ window.addEventListener('resize', syncDrawerHeight, { passive: true });
+});
onBeforeUnmount(() => {
window.removeEventListener('resize', onWinResizeScope);
+ window.removeEventListener('scroll', syncDrawerHeight, { capture: true });
+ window.removeEventListener('resize', syncDrawerHeight);
scopeResizeObs?.disconnect();
scopeResizeObs = null;
});
@@ -688,6 +708,9 @@ function moveWidget(i: number, dir: -1 | 1): void {
* ------------------------------------------------------------------- */
const selectedIdx = ref<number | null>(null);
+/* Re-fit the drawer to the viewport when it opens / the target widget changes
+ * (content height differs); the scroll + resize listeners keep it fitted
after. */
+watch(selectedIdx, () => void nextTick(syncDrawerHeight));
/** When the user switches scope or layer we drop the selection so the
* drawer doesn't refer to a widget that no longer exists. */
@@ -910,6 +933,14 @@ async function addAndSelectWidget(): Promise<void> {
addWidget();
await nextTick();
selectedIdx.value = currentWidgets.value.length - 1;
+ await nextTick();
+ /* The new widget is appended at the canvas bottom — on a tall board it lands
+ * below the fold. Scroll it into view so the operator sees the widget next
to
+ * the editor that just opened, instead of an empty drawer over off-screen
+ * canvas. block:'center' keeps the editor header + footer in frame too. */
+ canvasEl.value
+ ?.querySelector('.canvas-widget.selected')
+ ?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
/* ------------------------------------------------------------------- *
@@ -3761,7 +3792,7 @@ const namingTest = computed<NamingTestResult>(() => {
</div>
</div>
- <aside v-if="selectedWidget" class="drawer">
+ <aside v-if="selectedWidget" ref="drawerEl" class="drawer">
<div class="drawer-head">
<h4>Edit widget</h4>
<span class="sub">{{ scopeLabel(activeScope) }} · #{{
(selectedIdx ?? 0) + 1 }}</span>
@@ -3947,6 +3978,10 @@ const namingTest = computed<NamingTestResult>(() => {
<span>Layer-scoped (run MQE across the whole layer, ignore
selected service)</span>
</label>
</div>
+ </div>
+ <!-- Pinned footer: move/delete stay visible no matter how long
+ the form scrolls (the body above owns the overflow). -->
+ <div class="drawer-foot">
<div class="d-actions">
<button
class="sw-btn"
@@ -5503,7 +5538,10 @@ const namingTest = computed<NamingTestResult>(() => {
position: sticky;
top: 0;
align-self: start;
- height: calc(100vh - 52px);
+ /* Height is set in JS (syncDrawerHeight) to the live space from the drawer's
+ * top to the viewport bottom, so the pinned footer never falls below the
+ * fold. This calc is just the pre-JS / no-JS fallback. */
+ height: calc(100vh - 96px);
max-height: calc(100vh - 52px);
}
.drawer-head {
@@ -5696,12 +5734,15 @@ const namingTest = computed<NamingTestResult>(() => {
margin-top: 2px;
accent-color: var(--sw-accent);
}
+.drawer-foot {
+ flex-shrink: 0;
+ padding: 8px 14px;
+ border-top: 1px solid var(--sw-line);
+ background: var(--sw-bg-1);
+}
.d-actions {
display: flex;
gap: 6px;
- padding-top: 4px;
- border-top: 1px dashed var(--sw-line);
- margin-top: 4px;
}
.d-actions .sw-btn { font-size: 11px; }
.d-actions .sw-btn.danger { margin-left: auto; }