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}`);
 });
 

Reply via email to