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`
 

Reply via email to