Fix collapse, group skew, empty tables and the tab strip scrollbar

This commit is contained in:
2026-08-31 15:01:24 -04:00
parent 2fce339735
commit b21503e250
7 changed files with 70 additions and 16 deletions

View File

@@ -1083,6 +1083,21 @@ td.actions .btn + .btn {
display: contents; 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 Cards */
.section-card { .section-card {
background: var(--bg-card); background: var(--bg-card);

View File

@@ -70,11 +70,21 @@ function onKey(event, index) {
.adt { .adt {
display: flex; display: flex;
gap: 2px; 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-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.adt-tab { .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: inherit;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;

View File

@@ -191,18 +191,30 @@ const props = defineProps({
const panels = ref([]) const panels = ref([])
// The server resolves every panel's group before we see it, so an unknown or // The groups this build renders. A panel in any other group would be fetched,
// absent one is already 'details' by the time it arrives here. Filtering can // held, and shown on no tab at all.
// therefore be a plain equality check and nothing falls through the gap. //
// 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(() => 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 // Which groups actually have a panel with data, so a parent can hide a tab
// rather than offering an empty one. // rather than offering an empty one.
const populatedGroups = computed(() => { const populatedGroups = computed(() => {
const seen = new Set() 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 return seen
}) })

View File

@@ -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) { export function panelVisible(panel) {
if (panel.render === 'tabs') return tabSections(panel).length > 0 if (panel.render === 'tabs') return tabSections(panel).length > 0
if (panel.render === 'list') return rows(panel).length > 0 || !!panel.empty if (panel.render === 'list') return rows(panel).length > 0 || !!panel.empty
if (panel.render === 'keyvalue') return keyvalueFields(panel).length > 0 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 if (panel.render === 'badge') return badges(panel).length > 0
return false return false
} }

View File

@@ -327,3 +327,16 @@ describe('facet chips on a card that shows a health dot', () => {
expect(facetedRows(services, ['Start Pending']).length).toBe(1) 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)
})
})

View File

@@ -603,11 +603,6 @@ async function confirmRetire() {
</script> </script>
<style scoped> <style scoped>
/* Software and Activity are single long lists, so they use the full page width
rather than .content-grid's two balanced columns. Cards inside still need
their own vertical rhythm, which the column flow was providing. */
.detail-full { display: flex; flex-direction: column; gap: 1rem; }
.detail-full > * { min-width: 0; }
/* PC-specific styles - shared styles are in global style.css */ /* PC-specific styles - shared styles are in global style.css */

View File

@@ -462,17 +462,20 @@ class EndpointsPlugin(BasePlugin):
'group': 'metrics', 'group': 'metrics',
}, },
{ {
# Installed-software (SBOM) inventory. Low-priority reference # Installed-software (SBOM) inventory. Raw discovered list for
# data, not something looked at on every visit, so it starts
# collapsed and sits near the bottom. Raw discovered list for
# now: no managed-application linkage is surfaced yet. # now: no managed-application linkage is surfaced yet.
#
# NOT collapsible. It used to be, and started collapsed, because
# it was low-priority reference data sitting 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 - making them click again to see it is a step
# that asks a question already answered.
'id': 'endpoints-applications', 'id': 'endpoints-applications',
'title': 'Installed software', 'title': 'Installed software',
'assettypes': ['computer'], 'assettypes': ['computer'],
'endpoint': '/api/endpoints/asset/{assetid}/applications', 'endpoint': '/api/endpoints/asset/{assetid}/applications',
'render': 'table', 'render': 'table',
'collapsible': True,
'collapsedDefault': True,
'columns': [ 'columns': [
{'key': 'displayname', 'label': 'Name'}, {'key': 'displayname', 'label': 'Name'},
{'key': 'displayversion', 'label': 'Version'}, {'key': 'displayversion', 'label': 'Version'},