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/06/27 01:30:50 UTC
(skywalking-horizon-ui) 17/18: feat(dashboards): colored value-mapping cards (status chips)
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch refactor/decompose-large-files
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 7714858bd18721ebcfb2aa8979c8adc8c375f473
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sat Jun 27 09:28:37 2026 +0800
feat(dashboards): colored value-mapping cards (status chips)
A format:'enum' card now takes an optional color per value-map key (ok/warn/err/info/neutral) and renders each matched value — or metric label — as a colored status chip instead of a bare number. The K8s Node Status card uses it: its condition-labeled metric (Ready / *Pressure) renders as colored chips, not the raw count.
Also fixes the dashboard widget schema, which silently stripped format/valueMap/valueColors/labelTopN from the resolver's request body — so an enum card's value/color maps now reach the BFF resolution and the labeled metric keeps its labels. valueColors editing is wired into the layer-dashboard admin's value-map editor.
---
CHANGELOG.md | 6 ++
apps/bff/src/bundled_templates/layers/k8s.json | 9 ++-
apps/bff/src/http/query/dashboard.ts | 10 ++++
apps/bff/src/logic/dashboard/schema.ts | 6 ++
.../admin/layer-templates/LayerDashboardsAdmin.vue | 41 ++++++++++++-
.../src/render/layer-dashboard/LayerWidgetTile.vue | 68 +++++++++++++++++++++-
packages/api-client/src/dashboard.ts | 11 ++++
7 files changed, 147 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd59520..df9a00f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,12 @@ The version line is shared by every package in the monorepo (apps + shared packa
- **Denser Kubernetes dashboard tables** — the K8s layer's table widgets show more rows without scrolling.
+### Dashboards
+
+- **Cards can render values as colored status chips.** A card widget with `format: enum` now takes an optional chip color per value-map entry — `ok` (green), `warn` (amber), `err` (red), `info` (blue), `neutral` (grey) — and renders each matched value, or metric label, as a colored chip instead of a bare number. Set it in the layer-dashboard admin's value-map editor, next to the existing value → label mapping.
+
+- **The Kubernetes Node Status card now reads as a status, not a number.** Instead of a raw `1`, it shows the node's active conditions as colored chips — `Ready` in green, the `*Pressure` / `NetworkUnavailable` conditions in amber/red — so node health is legible at a glance.
+
## 1.0.0
### Performance & behavior tuning
diff --git a/apps/bff/src/bundled_templates/layers/k8s.json b/apps/bff/src/bundled_templates/layers/k8s.json
index f82745a..43e5e13 100644
--- a/apps/bff/src/bundled_templates/layers/k8s.json
+++ b/apps/bff/src/bundled_templates/layers/k8s.json
@@ -302,7 +302,14 @@
],
"span": 3,
"rowSpan": 1,
- "format": "int"
+ "format": "enum",
+ "valueColors": {
+ "Ready": "ok",
+ "MemoryPressure": "warn",
+ "DiskPressure": "warn",
+ "PIDPressure": "warn",
+ "NetworkUnavailable": "err"
+ }
},
{
"id": "node_pod_total",
diff --git a/apps/bff/src/http/query/dashboard.ts b/apps/bff/src/http/query/dashboard.ts
index e593472..4f775de 100644
--- a/apps/bff/src/http/query/dashboard.ts
+++ b/apps/bff/src/http/query/dashboard.ts
@@ -492,6 +492,16 @@ export function registerDashboardQueryRoute(app: FastifyInstance, deps: Dashboar
}
if (widget.type === 'card') {
+ // A colored enum card whose metric is labeled (e.g. K8s node
+ // conditions → one row per active condition) keeps the labels so
+ // the tile renders them as colored status chips; every other card
+ // collapses to the scalar average as before.
+ if (widget.format === 'enum' && widget.valueColors) {
+ const rows = parseTable(data[`w${wIdx}_e0`]);
+ if (rows && rows.some((r) => r.labels.length > 0)) {
+ return { id: widget.id, table: rows };
+ }
+ }
const first = widget.expressions.map((_, eIdx) =>
parseSeries(data[`w${wIdx}_e${eIdx}`]),
).find((s) => s !== null);
diff --git a/apps/bff/src/logic/dashboard/schema.ts b/apps/bff/src/logic/dashboard/schema.ts
index f7c5e04..b607004 100644
--- a/apps/bff/src/logic/dashboard/schema.ts
+++ b/apps/bff/src/logic/dashboard/schema.ts
@@ -43,6 +43,12 @@ const leafWidgetSchema = z.object({
showTableValues: z.boolean().optional(),
span: z.number().int().min(1).max(12).optional(),
rowSpan: z.number().int().min(1).max(64).optional(),
+ // Card value formatting + enum maps. The resolver gates colored status
+ // chips on format:'enum' + valueColors, so both must survive validation.
+ format: z.enum(['int', 'decimal', 'compact', 'duration', 'enum']).optional(),
+ valueMap: z.record(z.string()).optional(),
+ valueColors: z.record(z.string()).optional(),
+ labelTopN: z.number().int().min(1).max(50).optional(),
// Structured visibility gate. `.catch(undefined)` makes the parse
// TOLERANT: a legacy free-text predicate (`"<metric> has value"` /
// `#entity.x`) left over in an OAP-stored dashboard resolves to
diff --git a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 5cfb2bd..d4b895e 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -1213,6 +1213,10 @@ function setValueMapKey(oldKey: string, newKey: string): void {
const label = w.valueMap[oldKey];
delete w.valueMap[oldKey];
w.valueMap[newKey] = label;
+ if (w.valueColors && oldKey in w.valueColors) {
+ w.valueColors[newKey] = w.valueColors[oldKey];
+ delete w.valueColors[oldKey];
+ }
}
function addValueMapRow(): void {
const w = editingWidget.value;
@@ -1227,6 +1231,30 @@ function removeValueMapRow(key: string): void {
if (!w || !w.valueMap) return;
delete w.valueMap[key];
if (Object.keys(w.valueMap).length === 0) delete w.valueMap;
+ if (w.valueColors) {
+ delete w.valueColors[key];
+ if (Object.keys(w.valueColors).length === 0) delete w.valueColors;
+ }
+}
+
+// Optional color per valueMap key → `valueColors`, turning the enum card into
+// colored status chips (one per matched value/label). Empty = no chip color.
+const CHIP_COLORS = ['', 'ok', 'warn', 'err', 'info', 'neutral'] as const;
+function valueColorFor(key: string): string {
+ return editingWidget.value?.valueColors?.[key] ?? '';
+}
+function setValueColor(key: string, color: string): void {
+ const w = editingWidget.value;
+ if (!w) return;
+ if (!color) {
+ if (w.valueColors) {
+ delete w.valueColors[key];
+ if (Object.keys(w.valueColors).length === 0) delete w.valueColors;
+ }
+ return;
+ }
+ if (!w.valueColors) w.valueColors = {};
+ w.valueColors[key] = color;
}
/** Drawer commits edits in place on the live draft via v-model — no
@@ -4333,7 +4361,7 @@ const namingTest = computed<NamingTestResult>(() => {
<template v-else>
<template v-if="editingWidget">
<div v-if="editingWidget.format === 'enum'" class="d-section">
- <span class="d-label">Value map (enum → label)</span>
+ <span class="d-label">Value map (enum → label, color)</span>
<div class="vm-rows">
<div v-for="(row, i) in valueMapEntries" :key="i" class="vm-row">
<input
@@ -4349,11 +4377,19 @@ const namingTest = computed<NamingTestResult>(() => {
@input="setValueMapLabel(row[0], ($event.target as HTMLInputElement).value)"
placeholder="Failed"
/>
+ <select
+ class="vm-color"
+ :value="valueColorFor(row[0])"
+ title="Chip color"
+ @change="setValueColor(row[0], ($event.target as HTMLSelectElement).value)"
+ >
+ <option v-for="c in CHIP_COLORS" :key="c" :value="c">{{ c || '—' }}</option>
+ </select>
<button type="button" class="expr-del" title="Remove" @click="removeValueMapRow(row[0])">×</button>
</div>
</div>
<button type="button" class="sw-btn ghost small" @click="addValueMapRow">+ value</button>
- <p class="d-hint">Map a coded value to a label (e.g. 1 → OK). Card widgets only; labels are translatable per locale.</p>
+ <p class="d-hint">Map a coded value — or a metric label such as a K8s node condition — to a label and an optional chip color (<code>ok</code> green / <code>warn</code> amber / <code>err</code> red / <code>info</code> blue / <code>neutral</code> grey). With a color set, the card renders colored status chips, one per matched value. Card widgets only; labels are translatable per locale.</p>
</div>
<div class="d-section">
<span class="d-label">MQE expressions</span>
@@ -6428,6 +6464,7 @@ const namingTest = computed<NamingTestResult>(() => {
.vm-row .vm-key { width: 64px; flex: 0 0 auto; }
.vm-row .vm-arrow { color: var(--sw-fg-3); }
.vm-row .vm-label { flex: 1 1 auto; min-width: 0; }
+.vm-row .vm-color { width: 84px; flex: 0 0 auto; }
.expr-rows { display: flex; flex-direction: column; gap: 4px; }
.expr-row { display: flex; gap: 6px; align-items: center; }
.expr-row .expr-mqe { flex: 1 1 auto; min-width: 0; }
diff --git a/apps/ui/src/render/layer-dashboard/LayerWidgetTile.vue b/apps/ui/src/render/layer-dashboard/LayerWidgetTile.vue
index 13b4a32..75c62af 100644
--- a/apps/ui/src/render/layer-dashboard/LayerWidgetTile.vue
+++ b/apps/ui/src/render/layer-dashboard/LayerWidgetTile.vue
@@ -90,6 +90,33 @@ const { t } = useI18n({ useScope: 'global' });
function result(id: string): DashboardWidgetResult | undefined {
return props.results.get(id);
}
+
+// Colored enum card: the BFF keeps the metric's labels (one row per active
+// label — e.g. a K8s node's Ready / *Pressure conditions) instead of
+// collapsing to a scalar, so we render them as status chips. The chip key is
+// whichever label value the operator mapped in valueColors / valueMap.
+type ChipRow = NonNullable<DashboardWidgetResult['table']>[number];
+function chipKey(w: DashboardWidget, row: ChipRow): string {
+ for (const l of row.labels) {
+ if (l.value in (w.valueColors ?? {}) || l.value in (w.valueMap ?? {})) return l.value;
+ }
+ return row.labels.map((l) => l.value).join(' ') || String(row.value ?? '');
+}
+function chipLabel(w: DashboardWidget, row: ChipRow): string {
+ const k = chipKey(w, row);
+ return w.valueMap?.[k] ?? k;
+}
+function chipColor(w: DashboardWidget, row: ChipRow): 'ok' | 'warn' | 'err' | 'info' | 'neutral' {
+ const c = w.valueColors?.[chipKey(w, row)];
+ return c === 'ok' || c === 'warn' || c === 'err' || c === 'info' ? c : 'neutral';
+}
+// Only conditions the operator mapped are shown — keeps the status card on the
+// health conditions (Ready / *Pressure) and drops informational K8s noise.
+function chipRows(w: DashboardWidget, res: DashboardWidgetResult | undefined): ChipRow[] {
+ return (res?.table ?? []).filter((r) =>
+ r.labels.some((l) => l.value in (w.valueColors ?? {}) || l.value in (w.valueMap ?? {})),
+ );
+}
</script>
<template>
@@ -130,7 +157,15 @@ function result(id: string): DashboardWidgetResult | undefined {
<span class="muted">{{ result(widget.id)!.error }}</span>
</template>
<template v-else-if="widget.type === 'card'">
- <div v-if="compareMode" class="card-compare">
+ <div v-if="!compareMode && widget.valueColors && chipRows(widget, result(widget.id)).length" class="card-chips">
+ <span
+ v-for="(row, ci) in chipRows(widget, result(widget.id))"
+ :key="ci"
+ class="status-chip"
+ :class="`sc-${chipColor(widget, row)}`"
+ >{{ chipLabel(widget, row) }}</span>
+ </div>
+ <div v-else-if="compareMode" class="card-compare">
<div v-for="e in compareEntities" :key="e" class="cc-row">
<span class="cc-dot" :style="{ background: compareHue(e) }" />
<span class="cc-name" :title="entityLabel(e)">{{ entityLabel(e) }}</span>
@@ -370,6 +405,37 @@ function result(id: string): DashboardWidgetResult | undefined {
font-size: 11px;
color: var(--sw-fg-3);
}
+.card-chips {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ align-content: center;
+ gap: 6px;
+ height: 100%;
+ padding: 6px 0;
+}
+.status-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 10px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+.status-chip::before {
+ content: '';
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: currentColor;
+}
+.sc-ok { color: var(--sw-ok); background: var(--sw-ok-soft); }
+.sc-warn { color: var(--sw-warn); background: var(--sw-warn-soft); }
+.sc-err { color: var(--sw-err); background: var(--sw-err-soft); }
+.sc-info { color: var(--sw-info); background: var(--sw-info-soft); }
+.sc-neutral { color: var(--sw-fg-3); background: var(--sw-bg-2); }
.muted {
color: var(--sw-fg-3);
font-size: 11px;
diff --git a/packages/api-client/src/dashboard.ts b/packages/api-client/src/dashboard.ts
index bf13824..26b1613 100644
--- a/packages/api-client/src/dashboard.ts
+++ b/packages/api-client/src/dashboard.ts
@@ -197,6 +197,17 @@ export interface DashboardWidget {
* `slots`. Absent / no match ⇒ the numeric value is formatted.
*/
valueMap?: Record<string, string>;
+ /**
+ * Optional color per key, turning a `format: 'enum'` `card` into colored
+ * status chips. Keys match `valueMap` — a coded value (`"1"`), OR a metric
+ * **label** value for a label-keyed status (e.g. a K8s node condition
+ * `"Ready"` / `"MemoryPressure"`). Values are semantic tokens: `ok` (green),
+ * `warn` (amber), `err` (red), `info` (blue), `neutral` (grey). When set, a
+ * scalar enum renders its mapped label as one colored chip; a label-keyed
+ * metric renders every active label as its own chip (the BFF keeps the
+ * labels instead of collapsing to a scalar). Absent ⇒ plain enum/number.
+ */
+ valueColors?: Record<string, string>;
/**
* Column span in a 12-column flow grid. Default 4. Widgets pack via
* `grid-auto-flow: dense` so positions are dynamic — operators