You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/10/14 13:50:28 UTC
(skywalking-horizon-ui) 01/01: fix(traceql): the trace popout opens above the AI drawer, from the store it names
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch fix/traceql-popout-store-and-stacking
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 0359f32d03d6fbbc17c3c8b04c89873ceb833491
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Wed Oct 14 21:50:21 2026 +0800
fix(traceql): the trace popout opens above the AI drawer, from the store it names
A record's TraceQL trace opened from the AI assistant's panel had two faults.
The popout sat at z-index 40, under the AI drawer at 420, which covered its
right side with the close and view controls. Both TraceQL overlays now sit
at 999 with the native and Zipkin popouts, and the span dialog at 1000 above
them, as the native span dialog does.
On a layer's TraceQL row the global popout stood aside for any `?ds=`, and the
row opened the id from its own store, so an OTLP trace asked for on a TraceQL -
Native row was looked up in the native store and not found. The popout now
stands aside only on the row of the store it names, the row opens a URL trace
only from its own store, and closing it clears `ds` with the id. The row-to-
store mapping is one shared function rather than two copies.
---
apps/ui/src/layer/traceql/LayerTraceQLView.vue | 20 +++++++++++---------
apps/ui/src/layer/traceql/TraceQLSpanModal.vue | 3 ++-
apps/ui/src/layer/traceql/TraceQLTracePopout.vue | 3 ++-
.../ui/src/layer/traceql/traceqlDetailShared.test.ts | 12 +++++++++++-
apps/ui/src/layer/traceql/traceqlDetailShared.ts | 15 ++++++++++++++-
.../src/layer/traceql/useTraceQLTracePopout.test.ts | 11 ++++++++++-
apps/ui/src/layer/traceql/useTraceQLTracePopout.ts | 17 ++++++++---------
docs/changelog/1.1.0.md | 2 +-
8 files changed, 59 insertions(+), 24 deletions(-)
diff --git a/apps/ui/src/layer/traceql/LayerTraceQLView.vue b/apps/ui/src/layer/traceql/LayerTraceQLView.vue
index 2342b93c..9ea7e0c5 100644
--- a/apps/ui/src/layer/traceql/LayerTraceQLView.vue
+++ b/apps/ui/src/layer/traceql/LayerTraceQLView.vue
@@ -40,7 +40,7 @@ import TraceQLEditor from './TraceQLEditor.vue';
import TraceQLSchemaPanel from './TraceQLSchemaPanel.vue';
import TraceQLWindowFields from './TraceQLWindowFields.vue';
import { buildTraceQL, lintTraceQL, serviceInExpression, type BuilderRows } from './traceqlLint';
-import { TRACEQL_POPOUT_QUERY } from './traceqlDetailShared';
+import { TRACEQL_DS_QUERY, TRACEQL_POPOUT_QUERY, traceqlRowDatasource } from './traceqlDetailShared';
import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
import { TRACEQL_STATUS_VALUES } from '@/monaco/traceql-grammar';
import {
@@ -65,11 +65,7 @@ const time = useTimeRangeStore();
const layerKey = computed(() => routeEntryKey.value);
/** The row decides the store — each store has its own row. */
-const ds = computed<TraceQLDatasource>(() =>
- routeRow(route) === 'traceql-zipkin-trace' ? 'zipkin'
- : routeRow(route) === 'traceql-otlp-trace' ? 'otlp'
- : 'native',
-);
+const ds = computed<TraceQLDatasource>(() => traceqlRowDatasource(routeRow(route)) ?? 'native');
const { statusOf } = useTraceQLSources();
const status = computed(() => statusOf(ds.value));
@@ -232,15 +228,20 @@ const byId = useTraceQLTraceIds(ds, submittedIds);
* does. The overlay below is a different thing: a trace arrived at by URL,
* where there is no list for a card to sit beside. */
const openTraceId = ref<string | null>(null);
+/** Only a trace of THIS row's store: one asked for in another store is the
+ * global popout's to open, from the store it names. */
const urlTraceId = computed(() => {
const raw = route.query[TRACEQL_POPOUT_QUERY];
+ const asked = route.query[TRACEQL_DS_QUERY];
+ if (asked !== undefined && asked !== ds.value) return null;
return typeof raw === 'string' && raw ? raw : null;
});
-/** Clears only ITS key: wiping the whole query took the datasource and every
- * other page parameter with it. */
+/** Clears only ITS keys — the id and the store it names: wiping the whole
+ * query took every other page parameter with it. */
function closeUrlTrace(): void {
const query = { ...route.query };
delete query[TRACEQL_POPOUT_QUERY];
+ delete query[TRACEQL_DS_QUERY];
void router.replace({ query });
}
// Escape closes it, as it does the native trace popout. The shared helper
@@ -873,7 +874,8 @@ const issueText: Record<string, string> = {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
- z-index: 40;
+ /* With the native and Zipkin popouts, above the AI drawer (420). */
+ z-index: 999;
display: flex;
align-items: flex-start;
justify-content: center;
diff --git a/apps/ui/src/layer/traceql/TraceQLSpanModal.vue b/apps/ui/src/layer/traceql/TraceQLSpanModal.vue
index 5bec7d23..3d45b922 100644
--- a/apps/ui/src/layer/traceql/TraceQLSpanModal.vue
+++ b/apps/ui/src/layer/traceql/TraceQLSpanModal.vue
@@ -136,7 +136,8 @@ useEscapeToClose(
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
- z-index: 45;
+ /* Over its trace popout (999), as the native span dialog is. */
+ z-index: 1000;
display: flex;
/* Aligned from the TOP, like the trace popout it opens over, and inset from
it — centred and 82vh tall, this dialog was TALLER than that card, so the
diff --git a/apps/ui/src/layer/traceql/TraceQLTracePopout.vue b/apps/ui/src/layer/traceql/TraceQLTracePopout.vue
index 1926de62..b1526f86 100644
--- a/apps/ui/src/layer/traceql/TraceQLTracePopout.vue
+++ b/apps/ui/src/layer/traceql/TraceQLTracePopout.vue
@@ -40,7 +40,8 @@ useEscapeToClose(() => open.value !== null, closeTrace);
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
- z-index: 40;
+ /* With the native and Zipkin popouts, above the AI drawer (420). */
+ z-index: 999;
display: flex;
align-items: flex-start;
justify-content: center;
diff --git a/apps/ui/src/layer/traceql/traceqlDetailShared.test.ts b/apps/ui/src/layer/traceql/traceqlDetailShared.test.ts
index 384b5cb9..af40436f 100644
--- a/apps/ui/src/layer/traceql/traceqlDetailShared.test.ts
+++ b/apps/ui/src/layer/traceql/traceqlDetailShared.test.ts
@@ -27,7 +27,7 @@
import { describe, expect, it } from 'vitest';
import type { TraceQLSpan } from '@skywalking-horizon-ui/api-client';
-import { buildSpanTree, kindLabel, otlpKindName, rootSpanOf, spanStatus, statusColor } from './traceqlDetailShared';
+import { traceqlRowDatasource, buildSpanTree, kindLabel, otlpKindName, rootSpanOf, spanStatus, statusColor } from './traceqlDetailShared';
function span(spanId: string, parentSpanId: string, startUs = 0, durationUs = 10): TraceQLSpan {
return {
@@ -148,3 +148,13 @@ describe('the status a span is shown as', () => {
expect(spanStatus(withAttrs('error', { 'http.status_code': '200' }))).toBe('error');
});
});
+
+describe('traceqlRowDatasource', () => {
+ it('names the store of each TraceQL row, and none for any other row', () => {
+ expect(traceqlRowDatasource('traceql-native-trace')).toBe('native');
+ expect(traceqlRowDatasource('traceql-zipkin-trace')).toBe('zipkin');
+ expect(traceqlRowDatasource('traceql-otlp-trace')).toBe('otlp');
+ expect(traceqlRowDatasource('trace')).toBeNull();
+ expect(traceqlRowDatasource(undefined)).toBeNull();
+ });
+});
diff --git a/apps/ui/src/layer/traceql/traceqlDetailShared.ts b/apps/ui/src/layer/traceql/traceqlDetailShared.ts
index 447b0f4a..80178576 100644
--- a/apps/ui/src/layer/traceql/traceqlDetailShared.ts
+++ b/apps/ui/src/layer/traceql/traceqlDetailShared.ts
@@ -26,7 +26,7 @@
*/
import { spanOutcome } from '@skywalking-horizon-ui/api-client';
-import type { TraceQLSpan } from '@skywalking-horizon-ui/api-client';
+import type { TraceQLDatasource, TraceQLSpan } from '@skywalking-horizon-ui/api-client';
const SERVICE_PALETTE = [
'var(--sw-accent)', 'var(--sw-info)', 'var(--sw-cyan)', 'var(--sw-purple)',
@@ -165,6 +165,19 @@ export function latencyColor(durationUs: number): string {
* the timestamp hint the popout needs for an old id.
*/
export const TRACEQL_POPOUT_QUERY = 'traceqlId';
+/** The store the trace in {@link TRACEQL_POPOUT_QUERY} is read from. */
+export const TRACEQL_DS_QUERY = 'ds';
+
+/** The store a layer's TraceQL row reads — each store has its own row — or
+ * null for a row that is not a TraceQL one. */
+export function traceqlRowDatasource(row: string | undefined): TraceQLDatasource | null {
+ switch (row) {
+ case 'traceql-native-trace': return 'native';
+ case 'traceql-zipkin-trace': return 'zipkin';
+ case 'traceql-otlp-trace': return 'otlp';
+ default: return null;
+ }
+}
export interface SpanTreeRow {
span: TraceQLSpan;
diff --git a/apps/ui/src/layer/traceql/useTraceQLTracePopout.test.ts b/apps/ui/src/layer/traceql/useTraceQLTracePopout.test.ts
index 71eacfee..d27c41ab 100644
--- a/apps/ui/src/layer/traceql/useTraceQLTracePopout.test.ts
+++ b/apps/ui/src/layer/traceql/useTraceQLTracePopout.test.ts
@@ -28,6 +28,7 @@ async function harness(path: string) {
routes: [
{ path: '/layer/:layerKey/service', component: Blank, meta: { layerRow: 'service' } },
{ path: '/layer/:layerKey/traceql-otlp-trace', component: Blank, meta: { layerRow: 'traceql-otlp-trace' } },
+ { path: '/layer/:layerKey/traceql-native-trace', component: Blank, meta: { layerRow: 'traceql-native-trace' } },
],
});
await router.push(path);
@@ -52,11 +53,19 @@ describe('useTraceQLTracePopout', () => {
expect(api.open.value).toBeNull();
});
- it('stands aside on a TraceQL row, whose view opens the id itself', async () => {
+ it('stands aside on the TraceQL row of the store it names, whose view opens the id itself', async () => {
const { api } = await harness('/layer/otlp_service/traceql-otlp-trace?traceqlId=abc123&ds=otlp');
expect(api.open.value).toBeNull();
});
+ it('opens on a TraceQL row of another store, from the store it names', async () => {
+ const { router, api } = await harness('/layer/general/traceql-native-trace?traceqlId=abc123&ds=otlp');
+ expect(api.open.value).toEqual({ traceId: 'abc123', ds: 'otlp' });
+ api.closeTrace();
+ await new Promise((r) => setTimeout(r));
+ expect(router.currentRoute.value.query).toEqual({});
+ });
+
it('needs a known datasource', async () => {
const { api } = await harness('/layer/general/service?traceqlId=abc123&ds=tempo');
expect(api.open.value).toBeNull();
diff --git a/apps/ui/src/layer/traceql/useTraceQLTracePopout.ts b/apps/ui/src/layer/traceql/useTraceQLTracePopout.ts
index 66a72bf8..8de917bf 100644
--- a/apps/ui/src/layer/traceql/useTraceQLTracePopout.ts
+++ b/apps/ui/src/layer/traceql/useTraceQLTracePopout.ts
@@ -17,42 +17,41 @@
/** URL-backed popout for one trace read through a TraceQL store, over any
* page: `?traceqlId=` with `?ds=` naming the store, as the TraceQL card's
- * shareable URL writes them. The layer TraceQL rows read the same id with a
- * list beside it, so the popout stands aside on those routes. */
+ * shareable URL writes them. A layer's TraceQL row opens the same id itself
+ * — with a list beside it — but only from its own store, so the popout
+ * stands aside on the row of the store it was asked for, and on no other. */
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import type { TraceQLDatasource } from '@skywalking-horizon-ui/api-client';
import { routeRow } from '@/utils/layerRoute';
-import { TRACEQL_POPOUT_QUERY } from './traceqlDetailShared';
+import { TRACEQL_DS_QUERY, TRACEQL_POPOUT_QUERY, traceqlRowDatasource } from './traceqlDetailShared';
-const DS_QUERY = 'ds';
const DATASOURCES: readonly TraceQLDatasource[] = ['native', 'zipkin', 'otlp'];
export function useTraceQLTracePopout() {
const route = useRoute();
const router = useRouter();
- const onTraceQLRow = computed(() => routeRow(route)?.startsWith('traceql-') === true);
const open = computed<{ traceId: string; ds: TraceQLDatasource } | null>(() => {
- if (onTraceQLRow.value) return null;
const id = route.query[TRACEQL_POPOUT_QUERY];
- const ds = route.query[DS_QUERY];
+ const ds = route.query[TRACEQL_DS_QUERY];
if (typeof id !== 'string' || !id) return null;
if (typeof ds !== 'string' || !(DATASOURCES as readonly string[]).includes(ds)) return null;
+ if (traceqlRowDatasource(routeRow(route)) === ds) return null;
return { traceId: id, ds: ds as TraceQLDatasource };
});
function openTrace(id: string, ds: TraceQLDatasource): void {
if (!id) return;
- void router.push({ path: route.path, query: { ...route.query, [TRACEQL_POPOUT_QUERY]: id, [DS_QUERY]: ds } });
+ void router.push({ path: route.path, query: { ...route.query, [TRACEQL_POPOUT_QUERY]: id, [TRACEQL_DS_QUERY]: ds } });
}
function closeTrace(): void {
if (!open.value) return;
const query = { ...route.query };
delete query[TRACEQL_POPOUT_QUERY];
- delete query[DS_QUERY];
+ delete query[TRACEQL_DS_QUERY];
void router.replace({ path: route.path, query });
}
diff --git a/docs/changelog/1.1.0.md b/docs/changelog/1.1.0.md
index 79eac8df..bb326a72 100644
--- a/docs/changelog/1.1.0.md
+++ b/docs/changelog/1.1.0.md
@@ -184,7 +184,7 @@ The **3D Infra Map** opens framed on the tiers that hold services. A deployment
**Every MQE field in the layer template editor can now be run where you type it.** A run button beside each expression opens a panel that picks a service (and, where the metric needs one, an instance, endpoint or process), picks a time range with the same precision-tabbed picker the top of the page uses (custom ranges included), fires the expression against your OAP and shows exactly what came back — a graph of the series it returned, aligned on one time axis with a legend per label, the [...]
-**A record widget's trace icon opens the store the trace is in.** The new `recordTrace` widget field names the store a `record` widget's trace ids belong to, with the store names of `traces.sources`, and the icon opens the trace in that store's popout over the dashboard; the widget editor sets it under **Record trace → Trace store**, and the AI assistant's record figures follow it too. The icon used to open every record's trace in the native popout, which holds no OTLP trace. Absent, it [...]
+**A record widget's trace icon opens the store the trace is in.** The new `recordTrace` widget field names the store a `record` widget's trace ids belong to, with the store names of `traces.sources`, and the icon opens the trace in that store's popout over the page — above the AI assistant's drawer as well, and from the store it names even on a layer's TraceQL row of another store; the widget editor sets it under **Record trace → Trace store**, and the AI assistant's record figures follo [...]
### Fixes