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 3d5f1ac fix(layer): jump-to-trace + copy on record widgets, fix the
unfulfilled tip (#58)
3d5f1ac is described below
commit 3d5f1ac26d8295b64b7da15d3c36560e7413db5e
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Thu Jun 18 19:08:11 2026 +0800
fix(layer): jump-to-trace + copy on record widgets, fix the unfulfilled tip
(#58)
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`