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 bd681a4 ui events: 200-event buffer, ~10-row popover, capture
operator clicks
bd681a4 is described below
commit bd681a4296314b9d6b7a92b43e6f4f17127b2aff
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 12:00:26 2026 +0800
ui events: 200-event buffer, ~10-row popover, capture operator clicks
Three EventTicker improvements so the timeline tells the full story
of what's happening:
- History cap bumped 50 → 200 and the per-route reset removed. The
buffer survives navigation so the operator sees the cross-page
trace ("I clicked Logs, then services loaded, then dashboard
rendered, then I clicked Instance, then …").
- Popover max-height tuned to ~10 rows; the rest scrolls. Keeps the
ticker from eating screen real estate while still showing the
recent window at a glance.
- New `controls/useClickTracking.ts` delegates clicks at
document.body and emits `click` events for `<button>` / `<a>` /
`[role="button"]` / `[data-event-click]` targets, with the visible
label (or aria-label / title) as the text. The EventTicker itself,
form inputs, and anything tagged `data-no-event-track` are
suppressed so the log isn't drowned in chart-tooltip noise. Wired
into AppShell at mount.
---
apps/ui/src/controls/eventLog.ts | 2 +-
apps/ui/src/controls/useClickTracking.ts | 92 ++++++++++++++++++++++++++++++++
apps/ui/src/shell/AppShell.vue | 7 +++
apps/ui/src/shell/EventTicker.vue | 4 +-
apps/ui/src/shell/router/index.ts | 12 ++---
5 files changed, 109 insertions(+), 8 deletions(-)
diff --git a/apps/ui/src/controls/eventLog.ts b/apps/ui/src/controls/eventLog.ts
index 6b178e3..70bbe4a 100644
--- a/apps/ui/src/controls/eventLog.ts
+++ b/apps/ui/src/controls/eventLog.ts
@@ -50,7 +50,7 @@ export interface FrameworkEvent {
durationMs?: number;
}
-const HISTORY_CAP = 50;
+const HISTORY_CAP = 200;
const events = ref<FrameworkEvent[]>([]);
let nextId = 1;
/** Open `start` events keyed by topic — used to compute duration when
diff --git a/apps/ui/src/controls/useClickTracking.ts
b/apps/ui/src/controls/useClickTracking.ts
new file mode 100644
index 0000000..b95f07a
--- /dev/null
+++ b/apps/ui/src/controls/useClickTracking.ts
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Delegated click tracker — surfaces operator clicks in the
+ * EventTicker so the timeline reads as a complete "I did X, then
+ * the framework did Y" story. Without this the ticker only shows
+ * what the framework loaded, never what the operator pressed to
+ * trigger the load.
+ *
+ * Captures `click` events at document.body and emits a `click`
+ * event when the target (or an ancestor up to 4 levels) is a
+ * recognisable action element — `<button>`, `<a>`, `[role="button"]`,
+ * or anything carrying `data-event-click="<label>"` for explicit
+ * opt-in.
+ *
+ * Suppressions:
+ * - Anything inside the EventTicker itself (`.ev-zone`).
+ * - Elements with `data-no-event-track` (sprinkle this on noisy
+ * UI bits — chart tooltips, expand toggles in lists, etc.).
+ * - Form inputs (`<input>`, `<textarea>`, `<select>`).
+ * - Empty-text targets (decorative icon buttons without a label).
+ */
+
+import { onBeforeUnmount, onMounted } from 'vue';
+import { pushEvent } from '@/controls/eventLog';
+
+const TRACK_SELECTOR =
+ 'button, a, [role="button"], [data-event-click]';
+const SUPPRESS_SELECTOR = '.ev-zone, [data-no-event-track], input, textarea,
select';
+const MAX_LABEL_LEN = 80;
+
+function describe(el: HTMLElement): string | null {
+ const explicit = el.getAttribute('data-event-click');
+ if (explicit) return explicit;
+ const aria = el.getAttribute('aria-label');
+ if (aria && aria.trim()) return aria.trim();
+ const text = el.textContent?.replace(/\s+/g, ' ').trim();
+ if (text) return text.length > MAX_LABEL_LEN ? text.slice(0, MAX_LABEL_LEN)
+ '…' : text;
+ const title = el.getAttribute('title');
+ if (title && title.trim()) return title.trim();
+ return null;
+}
+
+function findActionTarget(start: HTMLElement | null): HTMLElement | null {
+ if (!start) return null;
+ // Walk up at most a few levels — the click target is often a
+ // child <span> inside the real `<button>`/`<a>` element.
+ let el: HTMLElement | null = start;
+ for (let i = 0; i < 5 && el; i++) {
+ if (el.matches?.(SUPPRESS_SELECTOR)) return null;
+ if (el.matches?.(TRACK_SELECTOR)) return el;
+ el = el.parentElement;
+ }
+ return null;
+}
+
+function onClick(ev: MouseEvent): void {
+ // Skip non-primary clicks (right-click, middle-click open-in-new-tab).
+ if (ev.button !== 0) return;
+ const target = findActionTarget(ev.target as HTMLElement | null);
+ if (!target) return;
+ // Re-check the ancestor chain for a suppression marker the loop
+ // above might have skipped past (e.g. a `.ev-zone` wrapper higher up).
+ if (target.closest(SUPPRESS_SELECTOR)) return;
+ const label = describe(target);
+ if (!label) return;
+ pushEvent('click', 'info', `Clicked: ${label}`);
+}
+
+export function useClickTracking(): void {
+ onMounted(() => {
+ document.body.addEventListener('click', onClick, true);
+ });
+ onBeforeUnmount(() => {
+ document.body.removeEventListener('click', onClick, true);
+ });
+}
diff --git a/apps/ui/src/shell/AppShell.vue b/apps/ui/src/shell/AppShell.vue
index 2b4823e..b8f2991 100644
--- a/apps/ui/src/shell/AppShell.vue
+++ b/apps/ui/src/shell/AppShell.vue
@@ -23,6 +23,7 @@ import GlobalConnectivityBanner from
'./GlobalConnectivityBanner.vue';
import TracePopout from '@/layer/traces/TracePopout.vue';
import ZipkinTracePopout from '@/layer/traces/ZipkinTracePopout.vue';
import { ensureConfigBundle } from '@/controls/configBundle';
+import { useClickTracking } from '@/controls/useClickTracking';
// Kick the config preload once the shell mounts (i.e. after the auth
// guard has let the user through). All layer dashboard configs +
@@ -32,6 +33,12 @@ import { ensureConfigBundle } from '@/controls/configBundle';
onMounted(() => {
void ensureConfigBundle();
});
+
+// Global delegated click tracker — emits `click` events into the
+// EventTicker so the timeline shows what the operator pressed before
+// each framework load. See `controls/useClickTracking.ts` for the
+// suppression rules (the ticker itself, form inputs, decorative bits).
+useClickTracking();
</script>
<template>
diff --git a/apps/ui/src/shell/EventTicker.vue
b/apps/ui/src/shell/EventTicker.vue
index 2f635f2..73a10f3 100644
--- a/apps/ui/src/shell/EventTicker.vue
+++ b/apps/ui/src/shell/EventTicker.vue
@@ -136,7 +136,9 @@ const eventCount = computed<number>(() => all.value.length);
top: calc(100% + 4px);
left: 0;
right: 0;
- max-height: 360px;
+ /* ~10 rows visible at once; the rest scrolls. Row is ~24px
+ * (4px top + 4px bottom padding + 11px text + a touch of slack). */
+ max-height: 260px;
overflow-y: auto;
z-index: 50;
background: var(--sw-bg-2);
diff --git a/apps/ui/src/shell/router/index.ts
b/apps/ui/src/shell/router/index.ts
index d292467..2e4f666 100644
--- a/apps/ui/src/shell/router/index.ts
+++ b/apps/ui/src/shell/router/index.ts
@@ -16,7 +16,7 @@
*/
import { createRouter, createWebHistory, type RouteRecordRaw } from
'vue-router';
import { useAuthStore } from '@/state/auth';
-import { pushEvent, resetEventLog } from '@/controls/eventLog';
+import { pushEvent } from '@/controls/eventLog';
const placeholder = () => import('@/shell/PlaceholderView.vue');
@@ -237,13 +237,13 @@ router.beforeEach(async (to) => {
}
});
-// Every successful navigation clears the event log + posts a single
-// "Navigated to X" line so the topbar EventTicker shows what page the
-// operator just opened. Subsequent data loaders push their own start /
-// ok / err events on top of this baseline.
+// Every successful navigation posts a single "Navigated to X" line so
+// the EventTicker shows when each page started loading. The event
+// buffer is intentionally NOT cleared on navigation — operators want
+// to see the cross-page history (last 200 events) so they can trace
+// "I clicked here, then services loaded, then I clicked there".
router.afterEach((to, from) => {
if (to.path === from.path) return;
- resetEventLog();
pushEvent('route', 'info', `Navigated to ${to.path}`);
});