You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/05/21 12:53:53 UTC
(skywalking-horizon-ui) 01/02: dashboard(table): one column per label dimension, not a joined string
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 <wu...@foxmail.com>
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;
}