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

commit 357fdd9922e7e2b113e06958f3aba4436e0d35d6
Author: Wu Sheng <[email protected]>
AuthorDate: Thu May 21 20:52:13 2026 +0800

    dashboard(table): one column per label dimension, not a joined string
    
    The table widget mashed all label values into a single "a · b" cell,
    which read poorly. Now each label dimension is its own column (e.g.
    Condition | Node, Deployment | Namespace | Replicas), with the optional
    value column last. parseTable returns per-row label arrays; the widget
    derives the column set from the union of label keys.
---
 apps/bff/src/http/query/dashboard.ts       | 19 ++++++++------
 apps/ui/src/render/widgets/TableWidget.vue | 40 +++++++++++++++++++++++-------
 packages/api-client/src/dashboard.ts       |  7 +++---
 3 files changed, 46 insertions(+), 20 deletions(-)

diff --git a/apps/bff/src/http/query/dashboard.ts 
b/apps/bff/src/http/query/dashboard.ts
index 71f13e0..5167b46 100644
--- a/apps/bff/src/http/query/dashboard.ts
+++ b/apps/bff/src/http/query/dashboard.ts
@@ -399,16 +399,12 @@ export function parseTopList(
  */
 export function parseTable(
   r: MqeResultShape | undefined,
-): Array<{ name: string; value: number | null }> | null {
+): Array<{ labels: Array<{ key: string; value: string }>; value: number | null 
}> | null {
   if (!r || r.error) return null;
   const results = r.results ?? [];
   if (results.length === 0) return null;
   const rows = results.map((rs) => {
-    const labels = rs.metric?.labels ?? [];
-    const name =
-      labels.length > 0
-        ? labels.map((l) => l.value).join(' · ')
-        : (rs.values?.[0]?.id ?? '—');
+    const labels = (rs.metric?.labels ?? []).map((l) => ({ key: l.key, value: 
l.value }));
     // `latest(...)` yields one bucket, but be defensive: take the last
     // non-null value across the result's buckets.
     let value: number | null = null;
@@ -417,9 +413,16 @@ export function parseTable(
       const n = Number(v.value);
       if (Number.isFinite(n)) value = n;
     }
-    return { name, value };
+    // No labels (degenerate) → fall back to the value id as a single column.
+    if (labels.length === 0 && rs.values?.[0]?.id) {
+      labels.push({ key: 'name', value: rs.values[0].id as string });
+    }
+    return { labels, value };
   });
-  rows.sort((a, b) => a.name.localeCompare(b.name));
+  // Stable order by the joined label values.
+  rows.sort((a, b) =>
+    a.labels.map((l) => l.value).join('·').localeCompare(b.labels.map((l) => 
l.value).join('·')),
+  );
   return rows;
 }
 
diff --git a/apps/ui/src/render/widgets/TableWidget.vue 
b/apps/ui/src/render/widgets/TableWidget.vue
index e240002..c37b182 100644
--- a/apps/ui/src/render/widgets/TableWidget.vue
+++ b/apps/ui/src/render/widgets/TableWidget.vue
@@ -29,6 +29,8 @@ import { fmtMetricAs } from '@/utils/formatters';
 const props = withDefaults(
   defineProps<{
     rows: DashboardTableRow[];
+    /** Optional value-column header; the label columns are headed by
+     *  their dimension key. `[, valueHeader]` — first entry unused. */
     headers?: [string, string];
     showValues?: boolean;
     unit?: string;
@@ -37,7 +39,28 @@ const props = withDefaults(
   { showValues: true },
 );
 
-const cols = computed(() => props.headers ?? ['Name', 'Value']);
+/** Ordered union of label dimension keys across all rows — one table
+ *  column per dimension (e.g. `condition`, `node`). */
+const labelKeys = computed<string[]>(() => {
+  const seen = new Set<string>();
+  const keys: string[] = [];
+  for (const r of props.rows) {
+    for (const l of r.labels) {
+      if (!seen.has(l.key)) {
+        seen.add(l.key);
+        keys.push(l.key);
+      }
+    }
+  }
+  return keys;
+});
+function titleCase(k: string): string {
+  return k.replace(/[_.]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
+}
+const valueHeader = computed(() => props.headers?.[1] || 'Value');
+function cell(r: DashboardTableRow, key: string): string {
+  return r.labels.find((l) => l.key === key)?.value ?? '—';
+}
 function fmt(v: number | null): string {
   if (v === null || v === undefined) return '—';
   const s = fmtMetricAs(v, props.format);
@@ -50,13 +73,13 @@ function fmt(v: number | null): string {
     <table class="tw__table">
       <thead>
         <tr>
-          <th>{{ cols[0] }}</th>
-          <th v-if="showValues" class="tw__num">{{ cols[1] }}</th>
+          <th v-for="k in labelKeys" :key="k">{{ titleCase(k) }}</th>
+          <th v-if="showValues" class="tw__num">{{ valueHeader }}</th>
         </tr>
       </thead>
       <tbody>
-        <tr v-for="(r, i) in rows" :key="`${r.name}-${i}`">
-          <td class="tw__name mono" :title="r.name">{{ r.name }}</td>
+        <tr v-for="(r, i) in rows" :key="i">
+          <td v-for="k in labelKeys" :key="k" class="tw__cell mono" 
:title="cell(r, k)">{{ cell(r, k) }}</td>
           <td v-if="showValues" class="tw__num mono">{{ fmt(r.value) }}</td>
         </tr>
       </tbody>
@@ -87,13 +110,12 @@ function fmt(v: number | null): string {
   border-bottom: 1px solid var(--sw-line-2, var(--sw-line));
   color: var(--sw-fg-1);
 }
-.tw__name {
-  max-width: 0;
-  width: 100%;
+.tw__cell {
+  max-width: 220px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
-.tw__num { text-align: right; white-space: nowrap; color: var(--sw-fg-0); }
+.tw__num { text-align: right; white-space: nowrap; color: var(--sw-fg-0); 
width: 1%; }
 .tw__table tbody tr:hover td { background: var(--sw-bg-2); }
 </style>
diff --git a/packages/api-client/src/dashboard.ts 
b/packages/api-client/src/dashboard.ts
index 99177b1..08774b4 100644
--- a/packages/api-client/src/dashboard.ts
+++ b/packages/api-client/src/dashboard.ts
@@ -193,10 +193,11 @@ export interface DashboardTopItem {
 }
 
 /** One row of a `table` widget — a single labeled result of a
- *  `latest(...)` metric. `name` is built from the result's label
- *  values (the status / phase / condition / entity dimensions). */
+ *  `latest(...)` metric. Each `labels` entry is one dimension
+ *  (status / phase / condition / entity), rendered as its own column;
+ *  `value` is the optional metric value column. */
 export interface DashboardTableRow {
-  name: string;
+  labels: Array<{ key: string; value: string }>;
   value: number | null;
 }
 

Reply via email to