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

Reply via email to