Rebuild the dashboard around what needs a person
The board was one card per source in a fixed grid: every source got the same small box whatever it had to say (warranties showed 5 of 50), and a box per source flattened priority, while the largest type on the page belonged to counts that need nobody. Now a severity rollup, one worst-first queue of everything needing a person, and a row of context beneath. The queue is full width, capped per source so a noisy check cannot bury a rare one, and names what did not fit with a link to it. Chips filter the queue by check as well as by severity; both narrow what is already loaded, so the counts and rows cannot disagree. Dropped: an 'All assets' tile shown beside its own four addends, a printer strip carrying the same count a third time, three cards whose job was to display 0, and a Recent Devices table listing five machines all 'In Use'. Contract 0.26.0 adds optional 'shortname' on dashboard cards - a title is written as a heading and ran wider than the hostname it qualified as a row badge. Also: the supplies card reported 'Low Supplies 0' when Zabbix was unreachable, which reads as reassurance rather than the truth that nothing could see the cartridges. It now distinguishes unreachable (says so) from unconfigured (stays quiet - the site declined the feature) from a real zero. Full suite 2182 passed, frontend 314 passed.
This commit is contained in:
@@ -3,6 +3,9 @@ import {
|
||||
toApiPath, rows, mapMeta, mapLink, cardRows, metricValue,
|
||||
cardVisible, sortCards, permittedCards, renderableCards,
|
||||
visibleRows, overflowCount,
|
||||
queueRows, queueOverflow, contextCards, severityTotals,
|
||||
QUEUEPERSOURCE, QUEUEMAX, singularise,
|
||||
cardUnavailable, unavailableCards, sourceFacets,
|
||||
} from './dashboardCards'
|
||||
|
||||
const failuresCard = {
|
||||
@@ -226,3 +229,242 @@ 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('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cardUnavailable', () => {
|
||||
// "Nothing is low" and "we cannot tell whether anything is low" are different
|
||||
// answers. The printer supplies card returned the same empty for both, so a
|
||||
// dead Zabbix rendered as "Low Supplies 0" - reassurance, not information.
|
||||
it('reads the reason a card gives for having no rows', () => {
|
||||
const card = { _data: { rows: [], unavailable: 'Supply monitoring is not reachable' } }
|
||||
expect(cardUnavailable(card)).toBe('Supply monitoring is not reachable')
|
||||
})
|
||||
|
||||
it('is empty for a card that simply has nothing to report', () => {
|
||||
expect(cardUnavailable({ _data: { rows: [] } })).toBe('')
|
||||
expect(cardUnavailable({ _data: [] })).toBe('')
|
||||
expect(cardUnavailable({ _data: null })).toBe('')
|
||||
})
|
||||
|
||||
it('ignores a non-string, so bad data cannot render as a reason', () => {
|
||||
expect(cardUnavailable({ _data: { rows: [], unavailable: true } })).toBe('')
|
||||
})
|
||||
|
||||
it('shows an unavailable card even when its empty rule says hide', () => {
|
||||
const card = {
|
||||
empty: 'hide', render: 'exceptions',
|
||||
_data: { rows: [], unavailable: 'Supply monitoring is not reachable' },
|
||||
}
|
||||
expect(cardVisible(card)).toBe(true)
|
||||
})
|
||||
|
||||
it('still hides a card that is merely empty', () => {
|
||||
expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: { rows: [] } })).toBe(false)
|
||||
})
|
||||
|
||||
it('still hides a card whose fetch failed, which is not the same thing', () => {
|
||||
expect(cardVisible({ empty: 'hide', render: 'exceptions', _data: null })).toBe(false)
|
||||
})
|
||||
|
||||
it('picks out the unavailable ones', () => {
|
||||
const blind = { id: 'a', _data: { rows: [], unavailable: 'not reachable' } }
|
||||
const fine = { id: 'b', _data: { rows: [{}] } }
|
||||
expect(unavailableCards([blind, fine]).map((c) => c.id)).toEqual(['a'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('sourceFacets', () => {
|
||||
it('is one chip per contributing check, worst first', () => {
|
||||
const facets = sourceFacets([warnA, critA, critB])
|
||||
expect(facets.map((f) => f.key)).toEqual(
|
||||
['geenforce-failures', 'endpoints-stability', 'computers-quiet'])
|
||||
})
|
||||
|
||||
it('labels with the shortname, since the title is a heading', () => {
|
||||
const card = { ...critA, shortname: 'Crashes' }
|
||||
expect(sourceFacets([card])[0].label).toBe('Crashes')
|
||||
})
|
||||
|
||||
it('falls back to the title when no shortname is declared', () => {
|
||||
expect(sourceFacets([critA])[0].label).toBe('Endpoints with crashes')
|
||||
})
|
||||
|
||||
it('counts every row, not just the ones the queue had room for', () => {
|
||||
// The chip says how many there are; the queue caps what it shows. A chip
|
||||
// reading 5 above a "3 more" line would be its own small lie.
|
||||
const noisy = { ...critA, _data: Array.from({ length: 12 }, (_, i) => ({ hostname: 'H' + i })) }
|
||||
expect(sourceFacets([noisy])[0].count).toBe(12)
|
||||
})
|
||||
|
||||
it('leaves out checks with nothing to show', () => {
|
||||
const empty = { ...critA, id: 'empty', _data: [] }
|
||||
expect(sourceFacets([empty])).toEqual([])
|
||||
})
|
||||
|
||||
it('leaves out info cards, which are not in the queue', () => {
|
||||
expect(sourceFacets([infoA])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user