Three faults, visible only once the board ran against production data. BACKUPS SAID THE WHOLE FLEET HAD STOPPED. The lastseenat backfill was wrong. It seeded from collectedat, reasoning that the last change was the last provable moment - but an unchanged config writes no revision, so a machine whose settings last changed nine months ago got a nine-month-old lastseenat and was instantly reported as a dead backup. Every chain lit up at once, which is worse than no card: it says the site is broken when it is fine. The honest value is NULL. Before the column existed nothing recorded when a config was last confirmed, and inventing a date does not change that. Migration 0003 clears the backfill, and staleness now IGNORES a NULL chain rather than substituting timestamps that mean something else. A chain becomes measurable the first time its PC posts, which for NTLARS is within a day. TONER READ "None%". The supply dict has no 'percent' key - it is 'remaining'. Supply names are also shortened, because "Black Toner Level 4%" spends three words saying what the card already says. THE CARDS READ AS WALLS OF TEXT. Rows wrapped into paragraphs and a card with forty PCs pushed everything below it off the screen. Now: at most five rows with "and N more", one line per row that truncates rather than wraps, meta pushed right and dropped first since it matters least, and severity reduced to a small dot beside an uppercase label instead of a coloured card - six severity-painted cards read as a crisis, which is how a board stops being read. Worth recording that none of this could fail in a test. Every one needed real data on a real fleet.
166 lines
5.8 KiB
JavaScript
166 lines
5.8 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
toApiPath, rows, mapMeta, mapLink, cardRows, metricValue,
|
|
cardVisible, sortCards, permittedCards, renderableCards,
|
|
visibleRows, overflowCount,
|
|
} from './dashboardCards'
|
|
|
|
const failuresCard = {
|
|
id: 'geenforce-failures',
|
|
render: 'exceptions',
|
|
severity: 'critical',
|
|
position: 10,
|
|
map: {
|
|
title: 'hostname',
|
|
detail: 'entryname',
|
|
meta: [{ key: 'message' }, { key: 'exitcode', label: 'exit' }],
|
|
link: '/pcs/{computerid}',
|
|
},
|
|
}
|
|
|
|
describe('endpoint paths', () => {
|
|
it('strips the api prefix the instance already carries', () => {
|
|
expect(toApiPath('/api/geenforce/dashboard/failures'))
|
|
.toBe('/geenforce/dashboard/failures')
|
|
})
|
|
|
|
it('leaves a relative endpoint alone', () => {
|
|
expect(toApiPath('/geenforce/x')).toBe('/geenforce/x')
|
|
})
|
|
})
|
|
|
|
describe('reading rows from whatever shape the endpoint returns', () => {
|
|
it('accepts a bare array, {rows} or {items}', () => {
|
|
expect(rows({ _data: [1, 2] })).toEqual([1, 2])
|
|
expect(rows({ _data: { rows: [1] } })).toEqual([1])
|
|
expect(rows({ _data: { items: [1, 2, 3] } })).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('treats a failed fetch as no rows rather than throwing', () => {
|
|
expect(rows({ _data: null })).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('mapping a row', () => {
|
|
it('builds title, detail, meta and link from the declaration', () => {
|
|
const card = { ...failuresCard, _data: [{
|
|
hostname: 'WJSF1234', entryname: 'Install OpenText',
|
|
exitcode: 1603, message: 'Fatal error', computerid: 42,
|
|
}] }
|
|
const [row] = cardRows(card)
|
|
expect(row.title).toBe('WJSF1234')
|
|
expect(row.detail).toBe('Install OpenText')
|
|
expect(row.meta.map((m) => m.text)).toEqual(['Fatal error', 'exit 1603'])
|
|
expect(row.link).toBe('/pcs/42')
|
|
})
|
|
|
|
it('supports a trailing unit so the text reads as a sentence', () => {
|
|
const card = {
|
|
render: 'exceptions',
|
|
map: { title: 'hostname', meta: [{ key: 'quietdays', label: 'quiet for', suffix: ' days' }] },
|
|
_data: [{ hostname: 'QUIETPC', quietdays: 3 }],
|
|
}
|
|
expect(cardRows(card)[0].meta[0].text).toBe('quiet for 3 days')
|
|
})
|
|
|
|
it('drops empty meta values instead of rendering a stray label', () => {
|
|
const card = { ...failuresCard, _data: [{ hostname: 'X', message: '', exitcode: null }] }
|
|
expect(cardRows(card)[0].meta).toEqual([])
|
|
})
|
|
|
|
it('omits the link when the substitution value is missing, keeping the row', () => {
|
|
// A PC shopdb does not know still reports its failure - that is the bay
|
|
// most likely to be misconfigured. It must not link to /pcs/undefined.
|
|
const card = { ...failuresCard, _data: [{ hostname: 'GHOSTPC', computerid: null }] }
|
|
const [row] = cardRows(card)
|
|
expect(row.title).toBe('GHOSTPC')
|
|
expect(row.link).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('long lists', () => {
|
|
const many = (n) => ({
|
|
render: 'exceptions',
|
|
map: { title: 'hostname' },
|
|
_data: Array.from({ length: n }, (_v, i) => ({ hostname: `PC${i}` })),
|
|
})
|
|
|
|
it('shows at most five rows so one card cannot bury the rest', () => {
|
|
expect(visibleRows(many(40))).toHaveLength(5)
|
|
expect(overflowCount(many(40))).toBe(35)
|
|
})
|
|
|
|
it('does not claim an overflow when everything fits', () => {
|
|
expect(visibleRows(many(3))).toHaveLength(3)
|
|
expect(overflowCount(many(3))).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('empty handling', () => {
|
|
it('hides a card with nothing to report by default', () => {
|
|
// The whole point: a card saying "nothing wrong" daily trains people to
|
|
// stop reading the page.
|
|
expect(cardVisible({ render: 'exceptions', _data: [] })).toBe(false)
|
|
})
|
|
|
|
it('keeps a card whose absence is itself news when it opts in', () => {
|
|
expect(cardVisible({ render: 'exceptions', _data: [], empty: 'line' })).toBe(true)
|
|
})
|
|
|
|
it('shows a card that has findings', () => {
|
|
expect(cardVisible({ render: 'exceptions', _data: [{ a: 1 }] })).toBe(true)
|
|
})
|
|
|
|
it('hides a card whose fetch failed rather than drawing it empty', () => {
|
|
expect(cardVisible({ render: 'exceptions', _data: null })).toBe(false)
|
|
})
|
|
|
|
it('hides a zero metric but shows a non-zero one', () => {
|
|
expect(cardVisible({ render: 'metric', _data: { value: 0 } })).toBe(false)
|
|
expect(cardVisible({ render: 'metric', _data: { value: 3 } })).toBe(true)
|
|
expect(metricValue({ render: 'metric', _data: { value: 3 } })).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('ordering', () => {
|
|
it('puts severity before position, so info never sits above a failure', () => {
|
|
const ordered = sortCards([
|
|
{ id: 'info-recent', severity: 'info', position: 1 },
|
|
{ id: 'crit-failures', severity: 'critical', position: 90 },
|
|
{ id: 'warn-toner', severity: 'warning', position: 50 },
|
|
])
|
|
expect(ordered.map((c) => c.id))
|
|
.toEqual(['crit-failures', 'warn-toner', 'info-recent'])
|
|
})
|
|
|
|
it('falls back to position within one severity', () => {
|
|
const ordered = sortCards([
|
|
{ id: 'b', severity: 'critical', position: 20 },
|
|
{ id: 'a', severity: 'critical', position: 10 },
|
|
])
|
|
expect(ordered.map((c) => c.id)).toEqual(['a', 'b'])
|
|
})
|
|
})
|
|
|
|
describe('gating', () => {
|
|
it('drops a card the user lacks permission for', () => {
|
|
// The dashboard must not become a way around RBAC.
|
|
const has = (name) => name === 'printers.view'
|
|
const kept = permittedCards([
|
|
{ id: 'toner', permission: 'printers.view' },
|
|
{ id: 'enforce', permission: 'geenforce.manage' },
|
|
{ id: 'open', permission: null },
|
|
], has)
|
|
expect(kept.map((c) => c.id)).toEqual(['toner', 'open'])
|
|
})
|
|
|
|
it('skips a render mode this core does not have', () => {
|
|
// A plugin built against a newer core degrades instead of leaving a hole.
|
|
const kept = renderableCards([
|
|
{ id: 'ok', render: 'exceptions' },
|
|
{ id: 'future', render: 'sparkline' },
|
|
])
|
|
expect(kept.map((c) => c.id)).toEqual(['ok'])
|
|
})
|
|
})
|