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; }
