This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch fix/record-widget-trace-jump in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit ddf9524033bb2b91442401885db6a78c8ba98fc7 Author: Wu Sheng <[email protected]> AuthorDate: Thu Jun 18 19:04:39 2026 +0800 fix(layer): jump-to-trace + copy on record widgets, fix the unfulfilled tip Record widgets (slow SQL / slow statements / slow cache commands) advertised "click to drill into the originating trace", but the trace id never reached the UI — the BFF selected only id/value/owner from the MQE response and dropped the per-sample trace. The tip promised a drill-in that couldn't happen. OAP's MQE exposes traceID on each RECORD_LIST sample (metrics-v3.graphqls). Select it, and forward it per row — omitted when the sample has none, since records are sampled and the trace can be absent. On the UI, the record widget now renders via a dedicated RecordList: each sampled row gets a jump-to-trace icon at the row head, shown ONLY when the sample carries a trace id, that opens the trace in the global popout (resolved by id, so it works even when the trace belongs to a service on another layer). The statement text itself is click-to-copy. Compare mode keeps the plain TopList (no single trace to jump to). Correct the bundled Virtual Database / Virtual Cache slow-record tips (English plus all 7 locale overlays) and the dashboard-widgets doc to describe the new copy + jump interaction instead of the old "click to drill" wording. --- CHANGELOG.md | 4 + .../layers/virtual_cache.i18n.de.json | 4 +- .../layers/virtual_cache.i18n.es.json | 4 +- .../layers/virtual_cache.i18n.fr.json | 4 +- .../layers/virtual_cache.i18n.ja.json | 4 +- .../layers/virtual_cache.i18n.ko.json | 4 +- .../layers/virtual_cache.i18n.pt.json | 4 +- .../layers/virtual_cache.i18n.zh-CN.json | 4 +- .../bundled_templates/layers/virtual_cache.json | 4 +- .../layers/virtual_database.i18n.de.json | 2 +- .../layers/virtual_database.i18n.es.json | 2 +- .../layers/virtual_database.i18n.fr.json | 2 +- .../layers/virtual_database.i18n.ja.json | 2 +- .../layers/virtual_database.i18n.ko.json | 2 +- .../layers/virtual_database.i18n.pt.json | 2 +- .../layers/virtual_database.i18n.zh-CN.json | 2 +- .../bundled_templates/layers/virtual_database.json | 2 +- apps/bff/src/http/query/dashboard.ts | 47 +++++- .../render/layer-dashboard/LayerDashboardsView.vue | 15 +- apps/ui/src/render/widgets/RecordList.vue | 183 +++++++++++++++++++++ docs/components/dashboard-widgets.md | 5 +- 21 files changed, 260 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1c641..04997f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ The version line is shared by every package in the monorepo (apps + shared packa - New **`duration`** format renders a SECONDS metric as a human time-ago (`5m 20s ago`; compact `5m` / `2h` on axes) — used by BanyanDB's **Time Since Last Sync** card. - Line-chart **axis labels and tooltips now use scientific notation** (`1.2e6`) for large magnitudes (≥ 10,000) so dense byte / count series stay readable, with the axis tick and its hovered value sharing one notation. +### Record widgets — jump to trace & copy + +- **Record widgets now drill into the originating trace.** Each sampled row gets a **jump-to-trace icon** at the row head — shown only when the sample actually carries a trace id (these are sampled, so it can be absent) — that opens the trace waterfall in the global popout. It resolves the trace **by id, not by layer**, so it works even though the trace belongs to the calling service on a different layer (a virtual-target layer has no traces tab of its own). The statement text itself is [...] + ### Instance-list badge - The badge on each row of the **instance list** (Containers / Pods / Nodes / …) is now **configurable per layer** (`instances.badge` on the layer template) — it can show any instance **attribute** instead of the fixed agent `language`. BanyanDB shows `container_name` (**liaison / data / lifecycle**), the role that actually distinguishes a container; agent-traced layers keep `language` (Java / Go / …). The badge is now **hidden when the value is empty or `UNKNOWN`**, so OpenTelemetry-scr [...] diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.de.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.de.json index 633d165..d355dca 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.de.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.de.json @@ -93,14 +93,14 @@ }, { "title": "Langsame Lesebefehle", - "tip": "Top 10 langsamste erfasste Lesebefehle. Jede Zeile ist eine einzelne Ausführung — klicken Sie, um in den auslösenden Trace einzutauchen.", + "tip": "Top 10 langsamste erfasste Lesebefehle. Jede Zeile ist eine einzelne Ausführung — klicken Sie zum Kopieren, oder nutzen Sie das Trace-Symbol am Zeilenanfang, um den auslösenden Trace zu öffnen (nur sichtbar, wenn das Sample einen enthält).", "expressions": [ null ] }, { "title": "Langsame Schreibbefehle", - "tip": "Top 10 langsamste erfasste Schreibbefehle. Jede Zeile ist eine einzelne Ausführung — klicken Sie, um in den auslösenden Trace einzutauchen.", + "tip": "Top 10 langsamste erfasste Schreibbefehle. Jede Zeile ist eine einzelne Ausführung — klicken Sie zum Kopieren, oder nutzen Sie das Trace-Symbol am Zeilenanfang, um den auslösenden Trace zu öffnen (nur sichtbar, wenn das Sample einen enthält).", "expressions": [ null ] diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.es.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.es.json index 6c3fa1e..1920ac7 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.es.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.es.json @@ -60,11 +60,11 @@ }, { "title": "Comandos de lectura lentos", - "tip": "Top 10 comandos de lectura más lentos capturados. Cada fila es una ejecución única — clic para profundizar en la traza originaria." + "tip": "Top 10 comandos de lectura más lentos capturados. Cada fila es una ejecución única — clic para copiar, o usa el icono de traza al inicio de la fila para abrir su traza originaria (visible solo cuando la muestra la incluye)." }, { "title": "Comandos de escritura lentos", - "tip": "Top 10 comandos de escritura más lentos capturados. Cada fila es una ejecución única — clic para profundizar en la traza originaria." + "tip": "Top 10 comandos de escritura más lentos capturados. Cada fila es una ejecución única — clic para copiar, o usa el icono de traza al inicio de la fila para abrir su traza originaria (visible solo cuando la muestra la incluye)." } ] } diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.fr.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.fr.json index aeb6d4d..ffe307c 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.fr.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.fr.json @@ -57,11 +57,11 @@ }, { "title": "Commandes de lecture lentes", - "tip": "Top 10 des commandes de lecture les plus lentes capturées. Chaque ligne est une exécution unique — cliquer pour explorer la trace d'origine." + "tip": "Top 10 des commandes de lecture les plus lentes capturées. Chaque ligne est une exécution unique — cliquer pour copier, ou utiliser l'icône de trace en tête de ligne pour ouvrir la trace d'origine (affichée uniquement lorsque l'échantillon en contient une)." }, { "title": "Commandes d'écriture lentes", - "tip": "Top 10 des commandes d'écriture les plus lentes capturées. Chaque ligne est une exécution unique — cliquer pour explorer la trace d'origine." + "tip": "Top 10 des commandes d'écriture les plus lentes capturées. Chaque ligne est une exécution unique — cliquer pour copier, ou utiliser l'icône de trace en tête de ligne pour ouvrir la trace d'origine (affichée uniquement lorsque l'échantillon en contient une)." } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ja.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ja.json index 9799e0d..dd12b1a 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ja.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ja.json @@ -57,11 +57,11 @@ }, { "title": "低速読み取りコマンド", - "tip": "捕捉された低速読み取りコマンド上位 10。各行は単一実行 — クリックで該当 Trace にドリルダウン。" + "tip": "捕捉された低速読み取りコマンド上位 10。各行は単一実行 — クリックでコピー、または行頭の Trace アイコンで該当 Trace を開く(サンプルに含まれる場合のみ表示)。" }, { "title": "低速書き込みコマンド", - "tip": "捕捉された低速書き込みコマンド上位 10。各行は単一実行 — クリックで該当 Trace にドリルダウン。" + "tip": "捕捉された低速書き込みコマンド上位 10。各行は単一実行 — クリックでコピー、または行頭の Trace アイコンで該当 Trace を開く(サンプルに含まれる場合のみ表示)。" } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ko.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ko.json index acabd50..5447830 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ko.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.ko.json @@ -57,11 +57,11 @@ }, { "title": "느린 읽기 명령", - "tip": "캡처된 가장 느린 읽기 명령 10개입니다. 각 행은 단일 실행이며 — 클릭하면 원본 트레이스로 이동합니다." + "tip": "캡처된 가장 느린 읽기 명령 10개입니다. 각 행은 단일 실행이며 — 클릭하면 복사되며, 행 머리의 트레이스 아이콘으로 원본 트레이스를 엽니다(샘플에 트레이스가 있을 때만 표시)." }, { "title": "느린 쓰기 명령", - "tip": "캡처된 가장 느린 쓰기 명령 10개입니다. 각 행은 단일 실행이며 — 클릭하면 원본 트레이스로 이동합니다." + "tip": "캡처된 가장 느린 쓰기 명령 10개입니다. 각 행은 단일 실행이며 — 클릭하면 복사되며, 행 머리의 트레이스 아이콘으로 원본 트레이스를 엽니다(샘플에 트레이스가 있을 때만 표시)." } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.pt.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.pt.json index 25014e0..0555e22 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.pt.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.pt.json @@ -60,11 +60,11 @@ }, { "title": "Comandos de leitura lentos", - "tip": "Top 10 comandos de leitura mais lentos capturados. Cada linha é uma execução única — clique para investigar o trace de origem." + "tip": "Top 10 comandos de leitura mais lentos capturados. Cada linha é uma execução única — clique para copiar, ou use o ícone de trace no início da linha para abrir o trace de origem (exibido apenas quando a amostra contém um)." }, { "title": "Comandos de escrita lentos", - "tip": "Top 10 comandos de escrita mais lentos capturados. Cada linha é uma execução única — clique para investigar o trace de origem." + "tip": "Top 10 comandos de escrita mais lentos capturados. Cada linha é uma execução única — clique para copiar, ou use o ícone de trace no início da linha para abrir o trace de origem (exibido apenas quando a amostra contém um)." } ] } diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.zh-CN.json b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.zh-CN.json index dbe7dde..023c1f8 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.zh-CN.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.i18n.zh-CN.json @@ -28,11 +28,11 @@ { "title": "写延迟分位" }, { "title": "慢读命令", - "tip": "采集到的最慢的 10 条读命令。每行是单次执行 — 点击可下钻到原始追踪。" + "tip": "采集到的最慢的 10 条读命令。每行是单次执行 — 点击可复制,或点击行首的 Trace 图标跳转到原始 Trace(仅在该采样包含 Trace 时显示)。" }, { "title": "慢写命令", - "tip": "采集到的最慢的 10 条写命令。每行是单次执行 — 点击可下钻到原始追踪。" + "tip": "采集到的最慢的 10 条写命令。每行是单次执行 — 点击可复制,或点击行首的 Trace 图标跳转到原始 Trace(仅在该采样包含 Trace 时显示)。" } ] } diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.json b/apps/bff/src/bundled_templates/layers/virtual_cache.json index 0193447..b4cedcd 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_cache.json +++ b/apps/bff/src/bundled_templates/layers/virtual_cache.json @@ -186,7 +186,7 @@ { "id": "slow_read", "title": "Slow Read Commands", - "tip": "Top 10 slowest captured read commands. Each row is a single execution \u2014 click to drill into the originating trace.", + "tip": "Top 10 slowest captured read commands. Each row is a single execution \u2014 click to copy it, or use the trace icon at the row head to open its originating trace (shown only when the sample carries one).", "type": "record", "unit": "ms", "expressions": [ @@ -198,7 +198,7 @@ { "id": "slow_write", "title": "Slow Write Commands", - "tip": "Top 10 slowest captured write commands. Each row is a single execution \u2014 click to drill into the originating trace.", + "tip": "Top 10 slowest captured write commands. Each row is a single execution \u2014 click to copy it, or use the trace icon at the row head to open its originating trace (shown only when the sample carries one).", "type": "record", "unit": "ms", "expressions": [ diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.de.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.de.json index f4589fb..d83a1fb 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.de.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.de.json @@ -48,7 +48,7 @@ }, { "title": "Langsame Anweisungen", - "tip": "Top 20 langsamste erfasste Anweisungen. Jede Zeile ist eine einzelne Anweisungsausführung — klicken Sie, um in den auslösenden Trace einzutauchen.", + "tip": "Top 20 langsamste erfasste Anweisungen. Jede Zeile ist eine einzelne Anweisungsausführung — klicken Sie zum Kopieren, oder nutzen Sie das Trace-Symbol am Zeilenanfang, um den auslösenden Trace zu öffnen (nur sichtbar, wenn das Sample einen enthält).", "expressions": [ null ] diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.es.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.es.json index 2696c3a..afe4fda 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.es.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.es.json @@ -34,7 +34,7 @@ }, { "title": "Statements lentos", - "tip": "Top 20 statements más lentos capturados. Cada fila es una ejecución única — clic para profundizar en la traza originaria." + "tip": "Top 20 statements más lentos capturados. Cada fila es una ejecución única — clic para copiar, o usa el icono de traza al inicio de la fila para abrir su traza originaria (visible solo cuando la muestra la incluye)." } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.fr.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.fr.json index bd316e0..724dee9 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.fr.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.fr.json @@ -36,7 +36,7 @@ }, { "title": "Requêtes lentes", - "tip": "Top 20 des statements les plus lents capturés. Chaque ligne est une exécution de statement unique — cliquer pour explorer la trace d'origine." + "tip": "Top 20 des statements les plus lents capturés. Chaque ligne est une exécution de statement unique — cliquer pour copier, ou utiliser l'icône de trace en tête de ligne pour ouvrir la trace d'origine (affichée uniquement lorsque l'échantillon en contient une)." } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ja.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ja.json index 0c89240..89c4986 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ja.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ja.json @@ -36,7 +36,7 @@ }, { "title": "低速ステートメント", - "tip": "捕捉された低速ステートメント上位 20。各行は単一ステートメント実行 — クリックで該当 Trace にドリルダウン。" + "tip": "捕捉された低速ステートメント上位 20。各行は単一ステートメント実行 — クリックでコピー、または行頭の Trace アイコンで該当 Trace を開く(サンプルに含まれる場合のみ表示)。" } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ko.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ko.json index 064f124..7dfc946 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ko.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.ko.json @@ -36,7 +36,7 @@ }, { "title": "느린 구문", - "tip": "캡처된 가장 느린 구문 20개입니다. 각 행은 단일 구문 실행이며 — 클릭하면 원본 트레이스로 이동합니다." + "tip": "캡처된 가장 느린 구문 20개입니다. 각 행은 단일 구문 실행이며 — 클릭하면 복사되며, 행 머리의 트레이스 아이콘으로 원본 트레이스를 엽니다(샘플에 트레이스가 있을 때만 표시)." } ] }, diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.pt.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.pt.json index 06aa7d2..b630eca 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.pt.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.pt.json @@ -39,7 +39,7 @@ }, { "title": "Statements lentos", - "tip": "Top 20 statements mais lentos capturados. Cada linha é uma execução única — clique para investigar o trace de origem." + "tip": "Top 20 statements mais lentos capturados. Cada linha é uma execução única — clique para copiar, ou use o ícone de trace no início da linha para abrir o trace de origem (exibido apenas quando a amostra contém um)." } ] } diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.zh-CN.json b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.zh-CN.json index 8aeaf09..376befa 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.i18n.zh-CN.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.i18n.zh-CN.json @@ -23,7 +23,7 @@ }, { "title": "慢语句", - "tip": "采集到的最慢的 20 条语句。每行是单条语句的一次执行 — 点击可下钻到原始追踪。" + "tip": "采集到的最慢的 20 条语句。每行是单条语句的一次执行 — 点击可复制,或点击行首的 Trace 图标跳转到原始 Trace(仅在该采样包含 Trace 时显示)。" } ] } diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.json b/apps/bff/src/bundled_templates/layers/virtual_database.json index 09cbf63..b09c6a8 100644 --- a/apps/bff/src/bundled_templates/layers/virtual_database.json +++ b/apps/bff/src/bundled_templates/layers/virtual_database.json @@ -101,7 +101,7 @@ { "id": "slow_statements", "title": "Slow Statements", - "tip": "Top 20 slowest captured statements. Each row is a single statement execution \u2014 click to drill into the originating trace.", + "tip": "Top 20 slowest captured statements. Each row is a single statement execution \u2014 click to copy it, or use the trace icon at the row head to open its originating trace (shown only when the sample carries one).", "type": "record", "unit": "ms", "expressions": [ diff --git a/apps/bff/src/http/query/dashboard.ts b/apps/bff/src/http/query/dashboard.ts index b679e4e..534af9c 100644 --- a/apps/bff/src/http/query/dashboard.ts +++ b/apps/bff/src/http/query/dashboard.ts @@ -171,6 +171,10 @@ interface MqeOwner { interface MqeValueShape { id?: string | null; value?: string | null; + /** Trace id of a RECORD_LIST sample (slow SQL / slow statements) — + * powers the jump-to-trace icon on a `record` widget. Null on + * non-record results. */ + traceID?: string | null; owner?: MqeOwner | null; } interface MqeLabelShape { @@ -299,7 +303,7 @@ export function buildFragment( ` type error\n` + ` results {\n` + ` metric { labels { key value } }\n` + - ` values { id value owner { scope serviceName serviceInstanceName endpointName } }\n` + + ` values { id value traceID owner { scope serviceName serviceInstanceName endpointName } }\n` + ` }\n` + ` }` ); @@ -403,6 +407,29 @@ export function parseTopList( }); } +/** + * Extract slow-SQL / slow-statement samples from a RECORD_LIST MQE + * response (e.g. `top_n(top_n_database_statement, …)`). Each entry is one + * sampled statement execution: `name` is the statement text (the MQE + * value `id`), `value` the latency, and `traceId` the originating trace — + * which the `record` widget surfaces as a jump-to-trace icon per row. + */ +export function parseRecords( + r: MqeResultShape | undefined, +): Array<{ name: string; value: number | null; traceId: string | null }> | null { + if (!r || r.error) return null; + const values = r.results?.[0]?.values ?? []; + if (values.length === 0) return null; + return values.map((v) => { + const num = v.value !== null && v.value !== undefined ? Number(v.value) : null; + return { + name: v.id ?? '—', + value: Number.isFinite(num as number) ? (num as number) : null, + traceId: v.traceID ?? null, + }; + }); +} + /** * Extract `table` rows from a LABELED `latest(...)` MQE response. Each * result is one label combination (e.g. `{phase: Running, service: x}` @@ -877,17 +904,19 @@ export function registerDashboardQueryRoute(app: FastifyInstance, deps: Dashboar } if (widget.type === 'record') { - // RECORD-typed MQE (slow SQL / slow statements) — the OAP - // response is owner-keyed like topList but each entry also - // carries a trace/segment id we surface for the drill-in. - // We reuse parseTopList for the name/value pair; the - // runtime record renderer is a separate phase and will - // promote v.id / v.refId fields once it lands. - const first = parseTopList(data[`w${wIdx}_e0`]); + // RECORD-typed MQE (slow SQL / slow statements). Each sample + // carries the originating trace id (MQE `traceID`), forwarded so + // the row can show a jump-to-trace icon; the statement text is + // click-to-copy on the UI side. + const first = parseRecords(data[`w${wIdx}_e0`]); if (!first) return { id: widget.id, error: 'no data' }; return { id: widget.id, - records: first.map((r) => ({ name: r.name, value: r.value })), + records: first.map((r) => ({ + name: r.name, + value: r.value, + ...(r.traceId ? { traceId: r.traceId } : {}), + })), }; } diff --git a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue index 5995e13..6f371f0 100644 --- a/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue +++ b/apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue @@ -31,6 +31,7 @@ import { useRoute } from 'vue-router'; import type { LayerDef } from '@skywalking-horizon-ui/api-client'; import TimeChart from '@/components/charts/TimeChart.vue'; import TopList from '@/components/charts/TopList.vue'; +import RecordList from '@/render/widgets/RecordList.vue'; import WidgetTip from '@/components/primitives/WidgetTip.vue'; import TableWidget from '@/render/widgets/TableWidget.vue'; import { colorForMetric } from '@/utils/metricColor'; @@ -1191,9 +1192,11 @@ function isHidden(id: string): boolean { <span v-else class="muted">{{ (compareMode ? compareLoading : (isFetching && !resultsById.has(w.id))) ? 'loading…' : 'no data' }}</span> </template> <template v-else-if="w.type === 'record'"> - <!-- Slow-statement / record table — reuses the TopList - renderer since the shape (name + value) is identical. - Future runtime work will add trace-id drill-in. --> + <!-- Slow-statement / record list. Compare mode falls back to + the plain TopList (name + value, no single trace to jump + to); single-entity uses RecordList, which adds the per-row + jump-to-trace icon (only when the sample carries a trace + id) and click-to-copy on the statement. --> <TopList v-if="compareMode && hasMultiTopData(w.id)" :ref="(el) => setTopListRef(w.id, el)" @@ -1202,13 +1205,11 @@ function isHidden(id: string): boolean { :color="widgetColor(w)" :title="w.title" /> - <TopList + <RecordList v-else-if="!compareMode && 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 }))" + :items="resultsById.get(w.id)!.records!" :unit="w.unit" :color="widgetColor(w)" - :title="w.title" /> <span v-else class="muted">{{ (compareMode ? compareLoading : (isFetching && !resultsById.has(w.id))) ? 'loading…' : 'no data' }}</span> </template> diff --git a/apps/ui/src/render/widgets/RecordList.vue b/apps/ui/src/render/widgets/RecordList.vue new file mode 100644 index 0000000..92e4d49 --- /dev/null +++ b/apps/ui/src/render/widgets/RecordList.vue @@ -0,0 +1,183 @@ +<!-- + 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. +--> +<!-- + Record-list widget — slow-SQL / slow-statement samples (a `record` + widget). Each row is one sampled statement execution: + + - a jump-to-trace icon at the head, shown ONLY when the sample carries + a trace id (these are sampled, so the trace can be absent); + - the statement text, click-to-copy; + - the metric value (latency) + unit. + + Compare mode keeps the plain TopList (no single trace to jump to); this + is the single-entity drill-in. +--> +<script setup lang="ts"> +import { ref } from 'vue'; +import Icon from '@/components/icons/Icon.vue'; +import { useTracePopout } from '@/layer/traces/useTracePopout'; +import { fmtMetric } from '@/utils/formatters'; + +interface RecordRow { + name: string; + value?: number | null; + traceId?: string; +} +defineProps<{ items: ReadonlyArray<RecordRow>; unit?: string; color?: string }>(); + +const { openTrace } = useTracePopout(); + +// Transient per-row "copied" flash so the copy click reads as an action. +const copiedIdx = ref<number | null>(null); +let copiedTimer: ReturnType<typeof setTimeout> | null = null; +async function copyStatement(text: string, idx: number): Promise<void> { + try { + await navigator.clipboard.writeText(text); + } catch { + /* clipboard blocked (insecure context / no permission) — no-op */ + } + copiedIdx.value = idx; + if (copiedTimer) clearTimeout(copiedTimer); + copiedTimer = setTimeout(() => { + copiedIdx.value = null; + }, 1200); +} +</script> + +<template> + <ul class="record-list"> + <li v-for="(r, i) in items" :key="i" class="rec-row"> + <!-- Jump-to-trace — only when this sample carries a trace id. --> + <button + v-if="r.traceId" + type="button" + class="rec-trace" + title="Open the originating trace" + @click="openTrace(r.traceId!)" + > + <Icon name="trace" :size="12" /> + </button> + <span v-else class="rec-trace-spacer" aria-hidden="true" /> + <!-- Statement text — click to copy. --> + <button + type="button" + class="rec-stmt" + :title="copiedIdx === i ? 'Copied to clipboard' : 'Click to copy statement'" + @click="copyStatement(r.name, i)" + > + <span class="rec-stmt-text">{{ r.name }}</span> + <span v-if="copiedIdx === i" class="rec-copied">copied</span> + </button> + <span class="rec-val" :style="{ color }"> + {{ fmtMetric(r.value ?? null) }}<span v-if="unit" class="rec-unit">{{ unit }}</span> + </span> + </li> + <li v-if="items.length === 0" class="rec-empty">no records</li> + </ul> +</template> + +<style scoped> +.record-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} +.rec-row { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-bottom: 1px solid var(--sw-line); + min-width: 0; +} +.rec-row:last-child { + border-bottom: none; +} +.rec-trace, +.rec-trace-spacer { + flex: 0 0 auto; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.rec-trace { + padding: 0; + border: 1px solid var(--sw-line-2); + border-radius: 4px; + background: var(--sw-bg-2); + color: var(--sw-accent-2); + cursor: pointer; +} +.rec-trace:hover { + border-color: var(--sw-accent-line); + background: var(--sw-accent-soft); +} +.rec-stmt { + flex: 1; + min-width: 0; + display: inline-flex; + align-items: baseline; + gap: 6px; + padding: 0; + background: transparent; + border: 0; + text-align: left; + cursor: pointer; + color: var(--sw-fg-1); +} +.rec-stmt-text { + font-family: var(--sw-mono); + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} +.rec-stmt:hover .rec-stmt-text { + color: var(--sw-fg-0); +} +.rec-copied { + flex: 0 0 auto; + font-size: 9.5px; + color: var(--sw-ok); + text-transform: uppercase; + letter-spacing: 0.06em; +} +.rec-val { + flex: 0 0 auto; + font-family: var(--sw-mono); + font-size: 11px; + font-variant-numeric: tabular-nums; + font-weight: 600; +} +.rec-unit { + margin-left: 2px; + font-size: 9.5px; + color: var(--sw-fg-3); +} +.rec-empty { + padding: 12px; + text-align: center; + color: var(--sw-fg-3); + font-size: 11px; + font-style: italic; +} +</style> diff --git a/docs/components/dashboard-widgets.md b/docs/components/dashboard-widgets.md index ba9f88d..d1166cc 100644 --- a/docs/components/dashboard-widgets.md +++ b/docs/components/dashboard-widgets.md @@ -174,8 +174,9 @@ The data source returns a record set (rows × typed columns) rather than a numer ### Behavior -- Renders as a dense table with column headers from the record's typed fields. -- Supports sort, filter, and pagination. +- Each row is one sampled record: the statement / command text, its value (e.g. latency), and — when the sample carries a trace id — a jump-to-trace icon at the row head that opens the originating trace in the trace viewer. +- Records are sampled, so a row's trace id can be absent; the jump-to-trace icon appears only when the sample has one. +- Click a row's statement text to copy it to the clipboard. ## `table`
