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 <[email protected]> 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
