Rebuild the dashboard around what needs a person
The board was one card per source in a fixed grid, and both halves of that hurt. Every source got the same small box whatever it had to say. Warranties showed 5 of 50 behind "and 45 more"; the crash card truncated at 4; a one-row card sat beside a four-row card and left a hole. Nothing had room, which is the complaint that started this: not enough space to relay the data. And a box per source flattened priority. An enforcement failure and a warranty notice looked equally urgent because each owned a card, while the largest type on the page belonged to counts that are true every day and need nobody. WHAT THE PAGE IS NOW A severity rollup, one worst-first queue of everything that needs a person, and a row of context beneath it. The queue merges every critical and warning row and sorts them, so reading order is work order. It is full width, which is where the room came from. Rows are capped per source, because fifty expiring warranties must not bury two crashed PCs, and what did not fit is named per source with a link to it - a bare "and 45 more" that leads nowhere is worse than saying nothing. The rollup counts ROWS, not cards, and filters the board in place rather than navigating away, so the counts and the rows cannot disagree. Severity, position and permission were already declared per card and already sorted on. The old layout threw that away; this only stops it. WHAT WENT, AND WHY IT SHOULD NOT COME BACK Four asset tiles plus an "All assets" tile: machines + PCs + printers + network IS all assets, so the page showed a total beside its own addends at the same weight. It is now a line under a rule, where a total belongs. The printer strip. It carried the printer count a third time - the tile row and "Total Printers" were already the same number - and "Online" equalled total, so three tiles held one fact. A genuinely low cartridge already reaches the queue through the printers-supplies card, which is the moment anyone wants it. Three cards whose whole job was to display 0. Recent Devices: the largest block on the page, listing five machines that were all "In Use" with business unit "TBD". Recency is not a reason to look at something. CONTRACT 0.26.0: 'shortname' Cards may declare a short label for their rows in the merged queue. Their 'title' is written as a heading - "Endpoints with crashes or shutdowns" - and as a row badge it ran wider than the hostname it qualified, putting the badge rather than the asset at the start of every line. CSS-truncating the title was tried first and was worse: four consecutive rows reading "ENDPOINTS WITH CR..." say nothing at all. Optional and defaults to the title, so a plugin that does not declare one is unaffected. Additive field, minor bump. ALSO: "silent 1 days" A meta suffix is written for the plural case and the card cannot know the value. One row saying it was a shrug; the merged queue puts every quiet endpoint on one screen, so it said it five times in a column and read as broken. Now singularised at exactly 1, on the first word, keeping what follows: "1 event (48h)", "silent 1 day". The test asserting ' ms' survives is what caught the first version turning it into ' m'. DashboardRow.vue is extracted because the queue and the context panels render the same row, and the map tooltip, link fallbacks and chips are too easy to get subtly different in a second copy. Verified on screen against live data. Full suite 2178 passed, frontend 301.
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
toApiPath, rows, mapMeta, mapLink, cardRows, metricValue,
|
||||
cardVisible, sortCards, permittedCards, renderableCards,
|
||||
visibleRows, overflowCount,
|
||||
queueRows, queueOverflow, contextCards, severityTotals,
|
||||
QUEUEPERSOURCE, QUEUEMAX, singularise,
|
||||
} from './dashboardCards'
|
||||
|
||||
const failuresCard = {
|
||||
@@ -226,3 +228,167 @@ describe('stacked rows', () => {
|
||||
.toBe('Start of a long message that continues well past the card')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The merged queue. These exist because the old grid failed in ways that were
|
||||
// invisible: a card truncated at 5 of 50 and nothing said where the rest went,
|
||||
// and severity was declared but the layout ignored it.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const critA = {
|
||||
id: 'endpoints-stability', title: 'Endpoints with crashes', render: 'exceptions',
|
||||
severity: 'critical', position: 20, viewall: '/pcs',
|
||||
map: { title: 'hostname' },
|
||||
_data: [{ hostname: 'PC-CMM-TEST' }, { hostname: 'HOST-7' }],
|
||||
}
|
||||
const critB = {
|
||||
id: 'geenforce-failures', title: 'Enforcement failures', render: 'exceptions',
|
||||
severity: 'critical', position: 10, viewall: '/geenforce',
|
||||
map: { title: 'hostname' },
|
||||
_data: [{ hostname: 'WJPC0615' }],
|
||||
}
|
||||
const warnA = {
|
||||
id: 'computers-quiet', title: 'PCs gone silent', render: 'exceptions',
|
||||
severity: 'warning', position: 30,
|
||||
map: { title: 'hostname' },
|
||||
_data: [{ hostname: 'FFBWTH63' }],
|
||||
}
|
||||
const infoA = {
|
||||
id: 'warranty-expiring', title: 'Warranties expiring', render: 'exceptions',
|
||||
severity: 'info', position: 40, viewall: '/reports/warranty',
|
||||
map: { title: 'assetnumber' },
|
||||
_data: Array.from({ length: 50 }, (_, i) => ({ assetnumber: 'A' + i })),
|
||||
}
|
||||
|
||||
describe('queueRows', () => {
|
||||
it('merges every queue card into one list, worst first', () => {
|
||||
const queue = queueRows([warnA, critA, critB])
|
||||
expect(queue.map((r) => r.severity)).toEqual(
|
||||
['critical', 'critical', 'critical', 'warning'])
|
||||
})
|
||||
|
||||
it('orders by declared position within a severity', () => {
|
||||
// geenforce is position 10, endpoints 20: a failure outranks a crash.
|
||||
const queue = queueRows([critA, critB])
|
||||
expect(queue[0].title).toBe('WJPC0615')
|
||||
})
|
||||
|
||||
it('keeps the source on every row, so a merged row can still say why', () => {
|
||||
const queue = queueRows([critB])
|
||||
expect(queue[0].source).toBe('Enforcement failures')
|
||||
expect(queue[0].sourceid).toBe('geenforce-failures')
|
||||
expect(queue[0].viewall).toBe('/geenforce')
|
||||
})
|
||||
|
||||
it('leaves info cards out - they are context, not work', () => {
|
||||
const queue = queueRows([critB, infoA])
|
||||
expect(queue).toHaveLength(1)
|
||||
expect(queue.every((r) => r.severity !== 'info')).toBe(true)
|
||||
})
|
||||
|
||||
it('caps one source so a noisy check cannot bury a rare one', () => {
|
||||
const noisy = { ...critA, _data: Array.from({ length: 40 }, (_, i) => ({ hostname: 'H' + i })) }
|
||||
const queue = queueRows([noisy, warnA])
|
||||
expect(queue.filter((r) => r.sourceid === 'endpoints-stability')).toHaveLength(QUEUEPERSOURCE)
|
||||
// the warning still made it onto the board
|
||||
expect(queue.some((r) => r.sourceid === 'computers-quiet')).toBe(true)
|
||||
})
|
||||
|
||||
it('caps the whole board', () => {
|
||||
const many = Array.from({ length: 6 }, (_, i) => ({
|
||||
...critA, id: 'c' + i, position: i,
|
||||
_data: Array.from({ length: 5 }, (_, j) => ({ hostname: 'H' + i + j })),
|
||||
}))
|
||||
expect(queueRows(many)).toHaveLength(QUEUEMAX)
|
||||
})
|
||||
|
||||
it('gives every row a stable key', () => {
|
||||
const keys = queueRows([critA, critB]).map((r) => r.key)
|
||||
expect(new Set(keys).size).toBe(keys.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('queueOverflow', () => {
|
||||
it('reports what was hidden, per source, with somewhere to go', () => {
|
||||
const noisy = { ...critA, _data: Array.from({ length: 12 }, (_, i) => ({ hostname: 'H' + i })) }
|
||||
const [entry] = queueOverflow([noisy])
|
||||
expect(entry.hidden).toBe(7) // 12 rows, 5 shown
|
||||
expect(entry.viewall).toBe('/pcs')
|
||||
expect(entry.title).toBe('Endpoints with crashes')
|
||||
})
|
||||
|
||||
it('says nothing when everything is on the board', () => {
|
||||
expect(queueOverflow([critB, warnA])).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores info cards, which are not in the queue to overflow from', () => {
|
||||
expect(queueOverflow([infoA])).toEqual([])
|
||||
})
|
||||
|
||||
it('lists the biggest backlog first', () => {
|
||||
const a = { ...critA, id: 'a', _data: Array.from({ length: 8 }, (_, i) => ({ hostname: 'x' + i })) }
|
||||
const b = { ...critA, id: 'b', _data: Array.from({ length: 20 }, (_, i) => ({ hostname: 'y' + i })) }
|
||||
expect(queueOverflow([a, b])[0].id).toBe('b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('contextCards', () => {
|
||||
it('is the info cards, sorted', () => {
|
||||
const context = contextCards([critA, infoA, warnA])
|
||||
expect(context.map((c) => c.id)).toEqual(['warranty-expiring'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('severityTotals', () => {
|
||||
it('counts ROWS, not cards', () => {
|
||||
// Three cards holding one row each is three things to do, not three cards.
|
||||
const totals = severityTotals([critA, critB, warnA, infoA])
|
||||
expect(totals).toEqual({ critical: 3, warning: 1, info: 50 })
|
||||
})
|
||||
|
||||
it('counts a metric card by its value', () => {
|
||||
const metric = { id: 'm', render: 'metric', severity: 'warning', _data: { value: 7 } }
|
||||
expect(severityTotals([metric]).warning).toBe(7)
|
||||
})
|
||||
|
||||
it('treats an undeclared severity as info, matching the sort', () => {
|
||||
const bare = { id: 'b', render: 'exceptions', _data: [{}, {}] }
|
||||
expect(severityTotals([bare]).info).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('singularise', () => {
|
||||
it('drops the s at exactly one', () => {
|
||||
expect(singularise(' days', 1)).toBe(' day')
|
||||
expect(singularise(' events', 1)).toBe(' event')
|
||||
})
|
||||
|
||||
it('leaves plurals alone', () => {
|
||||
expect(singularise(' days', 0)).toBe(' days')
|
||||
expect(singularise(' days', 2)).toBe(' days')
|
||||
expect(singularise(' days', 19)).toBe(' days')
|
||||
})
|
||||
|
||||
it('singularises the first word and keeps what follows', () => {
|
||||
// The card that made this matter: ' events (48h)' read "1 events (48h)"
|
||||
// five rows running once the queue merged.
|
||||
expect(singularise(' events (48h)', 1)).toBe(' event (48h)')
|
||||
expect(singularise(' events (48h)', 2)).toBe(' events (48h)')
|
||||
})
|
||||
|
||||
it('handles a trailing word rather than refusing it', () => {
|
||||
// "1 day ago" is correct; an earlier version left this plural out of
|
||||
// caution about chopping the wrong token.
|
||||
expect(singularise(' days ago', 1)).toBe(' day ago')
|
||||
})
|
||||
|
||||
it('leaves a unit that merely ends in s', () => {
|
||||
expect(singularise(' ms', 1)).toBe(' ms')
|
||||
expect(singularise('%', 1)).toBe('%')
|
||||
})
|
||||
|
||||
it('handles no suffix', () => {
|
||||
expect(singularise('', 1)).toBe('')
|
||||
expect(singularise(undefined, 1)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user