Fix collapse, group skew, empty tables and the tab strip scrollbar
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|
||||||
|
|||||||
@@ -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'},
|
||||||
|
|||||||
Reference in New Issue
Block a user