From c71183375fe8dc87ea80d324db84700e8a50965c Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 31 Aug 2026 15:01:14 -0400 Subject: [PATCH] Fix four things found by looking at the tabbed page SOFTWARE OPENED COLLAPSED. The installed-software panel was collapsible and started that way, because it used to be low-priority reference data near the bottom of one long page. It is now the Software tab's whole reason to exist, and somebody who clicked that tab has already said what they came for. Made it neither collapsible nor collapsed. TRACKED SERVICES VANISHED FROM METRICS. The group is decided by the SERVER, so a new frontend against a server still serving the old vocabulary puts a panel in a group the build does not render, and it is fetched, held, and shown nowhere - with nothing logged. Moving services out of 'actions' is exactly that shape. The client now falls back to 'details' for any group it does not render, the same way the server does, so a version-skewed pair degrades to a misplaced card instead of a missing one. A TABLE PANEL WITH NOTHING IN IT HID ITSELF. panelVisible honoured `empty` for list panels and ignored it for tables, for no reason anybody wrote down. A declared empty message is an answer - "No installed software reported." - where an absent card is a question. It matters more now that a panel can be the only thing in a tab, because the tab disappears with it. THE TAB STRIP HAD A VERTICAL SCROLLBAR. Per the overflow spec, when one axis is not `visible` the other computes from `visible` to `auto`, so `overflow-x: auto` alone made the strip scrollable vertically too and the -1px margin the tabs use to sit on the border was enough to trip it. Also moved .detail-full into the global stylesheet next to .content-grid, per frontend/CLAUDE.md: it is detail-page layout, and the other four detail pages need it when they adopt tabs. Scope: product --- frontend/src/assets/style.css | 15 +++++++++++++ frontend/src/components/AssetDetailTabs.vue | 10 +++++++++ frontend/src/components/PluginAssetPanels.vue | 22 ++++++++++++++----- frontend/src/components/pluginAssetPanels.js | 8 ++++++- .../src/components/pluginAssetPanels.spec.js | 13 +++++++++++ plugins/computers/frontend/views/PCDetail.vue | 5 ----- plugins/endpoints/plugin.py | 13 ++++++----- 7 files changed, 70 insertions(+), 16 deletions(-) diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 5038c546..7cf95e64 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -1083,6 +1083,21 @@ td.actions .btn + .btn { display: contents; } +/* Full-width alternative to .content-grid, for a tab holding one long list + rather than short fact cards. An application inventory or an event feed + split into two balanced columns reads as two unrelated lists, and the eye + has to hunt for where the first one ended. */ +.detail-full { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 25px; +} + +.detail-full > * { + min-width: 0; +} + /* Section Cards */ .section-card { background: var(--bg-card); diff --git a/frontend/src/components/AssetDetailTabs.vue b/frontend/src/components/AssetDetailTabs.vue index a4c49778..1cb9e335 100644 --- a/frontend/src/components/AssetDetailTabs.vue +++ b/frontend/src/components/AssetDetailTabs.vue @@ -70,11 +70,21 @@ function onKey(event, index) { .adt { display: flex; gap: 2px; + /* overflow-y MUST be stated. Per the spec, when one axis is not `visible` + the other computes from `visible` to `auto` - so `overflow-x: auto` alone + silently makes this scrollable vertically too, and the -1px margin the + tabs use to sit on the border is enough to trip it. The result is a tiny + vertical scrollbar inside the strip. */ overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; } .adt-tab { + /* Never shrink: a squeezed tab wraps its label, which grows the strip and is + what horizontal scrolling exists to avoid. */ + flex: none; font: inherit; font-size: 0.875rem; font-weight: 500; diff --git a/frontend/src/components/PluginAssetPanels.vue b/frontend/src/components/PluginAssetPanels.vue index a3d3d587..58155e5d 100644 --- a/frontend/src/components/PluginAssetPanels.vue +++ b/frontend/src/components/PluginAssetPanels.vue @@ -191,18 +191,30 @@ const props = defineProps({ const panels = ref([]) -// The server resolves every panel's group before we see it, so an unknown or -// absent one is already 'details' by the time it arrives here. Filtering can -// therefore be a plain equality check and nothing falls through the gap. +// The groups this build renders. A panel in any other group would be fetched, +// held, and shown on no tab at all. +// +// The server normalises group before we see it, so a version-matched pair +// never lands here. A MISMATCHED pair does: deploy a new frontend against a +// server still serving an older vocabulary and every panel in a group that +// has since been renamed or removed silently disappears. That is what happened +// to tracked services when it moved out of 'actions' - the backend still said +// 'actions', this build had no such tab, and the card was simply gone with +// nothing logged. Fall back the same way the server does rather than drop it. +const RENDERED_GROUPS = ['details', 'software', 'metrics', 'activity'] +function groupOf(panel) { + return RENDERED_GROUPS.includes(panel.group) ? panel.group : 'details' +} + const groupedPanels = computed(() => - props.group ? panels.value.filter((p) => p.group === props.group) : panels.value + props.group ? panels.value.filter((p) => groupOf(p) === props.group) : panels.value ) // Which groups actually have a panel with data, so a parent can hide a tab // rather than offering an empty one. const populatedGroups = computed(() => { const seen = new Set() - panels.value.forEach((p) => { if (panelVisible(p)) seen.add(p.group || 'details') }) + panels.value.forEach((p) => { if (panelVisible(p)) seen.add(groupOf(p)) }) return seen }) diff --git a/frontend/src/components/pluginAssetPanels.js b/frontend/src/components/pluginAssetPanels.js index 310e2485..739d9e74 100644 --- a/frontend/src/components/pluginAssetPanels.js +++ b/frontend/src/components/pluginAssetPanels.js @@ -246,11 +246,17 @@ export function rowAction(panel, item) { } } +// A panel declaring `empty` is asking to stay visible with nothing in it and +// say so - "No installed software reported." is an answer, where an absent +// card is a question. `list` honoured that and `table` did not, for no reason +// anybody wrote down, so a table panel with an empty message still vanished. +// It matters more now that a panel can be the only thing in a tab: the tab +// itself disappears with it. export function panelVisible(panel) { if (panel.render === 'tabs') return tabSections(panel).length > 0 if (panel.render === 'list') return rows(panel).length > 0 || !!panel.empty if (panel.render === 'keyvalue') return keyvalueFields(panel).length > 0 - if (panel.render === 'table') return rows(panel).length > 0 + if (panel.render === 'table') return rows(panel).length > 0 || !!panel.empty if (panel.render === 'badge') return badges(panel).length > 0 return false } diff --git a/frontend/src/components/pluginAssetPanels.spec.js b/frontend/src/components/pluginAssetPanels.spec.js index 5e3810db..f6377ced 100644 --- a/frontend/src/components/pluginAssetPanels.spec.js +++ b/frontend/src/components/pluginAssetPanels.spec.js @@ -327,3 +327,16 @@ describe('facet chips on a card that shows a health dot', () => { expect(facetedRows(services, ['Start Pending']).length).toBe(1) }) }) + +describe('panelVisible and empty', () => { + it('keeps a table panel that declares an empty message', () => { + // It is the only card in the Software tab, so hiding it hides the tab and + // the reader cannot tell "nothing reported" from "feature missing". + const panel = { render: 'table', _data: { items: [] }, empty: 'No installed software reported.' } + expect(panelVisible(panel)).toBe(true) + }) + + it('still hides a table panel with nothing to say', () => { + expect(panelVisible({ render: 'table', _data: { items: [] } })).toBe(false) + }) +}) diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 0f241544..9970e092 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -603,11 +603,6 @@ async function confirmRetire() {