merge storage and disk health into one card with two sections

Volume capacity and physical drive health were two adjacent cards, and the operator reading a full disk is usually the operator reading a dying disk in the same minute. They are now one Storage card with two sections. Visual grouping only - the two tables share no key, storage being per volume and health per physical device, and one drive can back several volumes or none the operator sees. Nothing is reconciled across the sections; this is not the mapped-drive merge where two sources described the same drive and the worse status won. The tabs render type was the only sections-in-one-card mechanism and emitted label/value fields with no table path, so it now takes table sections too. That is the renderer every plugin uses, so both behaviour changes in it are mutation-tested. Two things only surfaced on contact: the empty-section filter had to grow a table branch, because a table section declares columns up front and dropping it when it has no rows takes its tab with it and reads as no disks rather than nothing reported; and full-width span was keyed off the declared render type, but a tabs panel holding a six-column table needs the same span and that is only knowable from fetched data. The diskhealth route stays. It no longer backs a panel, but it is a stable read for callers outside the renderer and removing it for a UI-only change would break them.
This commit is contained in:
benjamin.baker
2026-09-05 07:05:30 -04:00
parent af42bf5abd
commit e00bb44d6b
13 changed files with 321 additions and 77 deletions

View File

@@ -3,7 +3,7 @@
a panel as JSON (title, endpoint, render mode, field map); this renders it
on any asset detail page, so a plugin adds detail-page UI with no Vue. -->
<template v-for="panel in groupedPanels" :key="panel.id">
<div v-if="panelVisible(panel)" class="section-card" :class="{ 'pap-span': panel.render === 'table' }">
<div v-if="panelVisible(panel)" class="section-card" :class="{ 'pap-span': spansFullWidth(panel) }">
<!-- collapsible panels get a clickable header with a caret and, when the
panel declares a status rollup, a green/yellow/red dot for the worst
row (a stopped service or a warning/critical event). --><h3
@@ -120,7 +120,39 @@
@click="setTab(panel, si)"
>{{ section.label }}</button>
</div>
<div class="pap-kv">
<!-- A section carrying columns renders as a table; otherwise it is the
original label/value grid. Both shapes coexist in one panel. -->
<div
v-if="((tabSections(panel)[activeTab(panel)] || {}).columns || []).length"
class="pap-table-wrap"
>
<table class="pap-table">
<thead>
<tr>
<th
v-for="col in (tabSections(panel)[activeTab(panel)] || {}).columns"
:key="col.key"
>{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, ri) in (tabSections(panel)[activeTab(panel)] || {}).rows || []"
:key="ri"
>
<td
v-for="col in (tabSections(panel)[activeTab(panel)] || {}).columns"
:key="col.key"
>{{ cell(row, col) }}</td>
</tr>
</tbody>
</table>
<div
v-if="!((tabSections(panel)[activeTab(panel)] || {}).rows || []).length"
class="pap-empty"
>{{ (tabSections(panel)[activeTab(panel)] || {}).empty || 'Nothing reported.' }}</div>
</div>
<div v-else class="pap-kv">
<div
v-for="(field, fi) in (tabSections(panel)[activeTab(panel)] || {}).fields || []"
:key="fi"
@@ -258,7 +290,7 @@ import { colorStyle } from '@/utils/colorStyle'
import {
toApiPath, rows, panelVisible, mapTitle, mapBadge, mapMeta, mapBody, manageLink,
keyvalueFields, tableColumns, cell, badges, tabSections, rollupStatus,
rowAction, facets, facetedRows, formatValue,
rowAction, facets, facetedRows, formatValue, spansFullWidth,
} from './pluginAssetPanels'
// Operator report screenshots are served by a JWT-gated route, so a bare