Files
shopdb-flask/frontend/src/components/pluginAssetPanels.spec.js
benjamin.baker 9ac711ce2d Surface the endpoint inventory, and let an event show its message
The hardware, OS and storage inventory had columns and data but nowhere to
be read: the only per-PC card was the status keyvalue, so a machine's
processor, firmware, disks and listening ports were reachable only by
querying the database.

Adds four Monitors cards - Hardware, Operating system, Storage, Listening
ports - split by the question each answers rather than by which JSON blob
they came from. OS and BIOS share one card deliberately; apart they are
three short rows each. A card yields no fields when its columns are null,
so an endpoint on an older agent hides them rather than showing a grid of
blanks.

User context is declared last on the page. The probe feeding it is not yet
producing output, so it reads empty for now, and being last means an empty
card sits below the populated ones instead of pushing them down.

The Activity feed showed a title and an event id, but the title is the
catalog name for that id - the same text on every occurrence - so two
different faults read identically. Rows now carry the provider name, and
the stored message body expands in place when the severity badge is
clicked. Commands expand to their result message, which is the reason a
failed one failed.

The badge doubles as the expander rather than gaining a control beside it,
and it keeps its severity label when open: swapping that for "Hide" would
cost the at-a-glance severity scan down the feed. Being a button rather
than a span, it is also now keyboard reachable.

map.body is additive in the shared renderer - panels that do not declare
it render exactly as before.
2026-09-04 19:14:16 -04:00

360 lines
14 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
toApiPath, subst, rows, mapTitle, mapBadge, mapMeta, mapBody, manageLink,
keyvalueFields, tableColumns, badges, panelVisible, tabSections, rollupStatus,
rowAction, facets, facetedRows,
} from './pluginAssetPanels'
const warrantyPanel = {
id: 'warranty', title: 'Warranty', render: 'list',
map: {
title: 'vendor',
badge: {
label: 'status', color: 'statuscolor',
labelmap: { active: 'Active', expired: 'Expired' },
},
meta: [
{ key: 'servicelevel' },
{ key: 'enddate', label: 'Ends', format: 'date' },
{ key: 'servicetag', label: 'Tag', mono: true },
],
},
empty: 'No warranty on record.',
manage: { to: '/warranties?addfor={assetid}', label: 'Add / manage' },
_data: [
{
vendor: 'Dell', status: 'active', statuscolor: '#4CAF50',
servicelevel: 'ProSupport', enddate: '2027-01-01', servicetag: 'ABC123',
},
],
}
describe('endpoint + subst', () => {
it('substitutes {assetid} and strips the leading /api', () => {
expect(toApiPath('/api/warranty/asset/{assetid}', 5)).toBe('/warranty/asset/5')
expect(subst('/warranties?addfor={assetid}', 9)).toBe('/warranties?addfor=9')
})
it('only strips a leading /api segment, not /apixyz', () => {
expect(toApiPath('/apixyz/thing', 1)).toBe('/apixyz/thing')
})
})
describe('list render mapping (warranty)', () => {
it('maps title, badge (labelmap + color), and formatted meta', () => {
const item = rows(warrantyPanel)[0]
expect(mapTitle(warrantyPanel, item)).toBe('Dell')
const badge = mapBadge(warrantyPanel, item)
expect(badge.label).toBe('Active')
expect(badge.color).toBe('#4CAF50')
const meta = mapMeta(warrantyPanel, item)
expect(meta[0].text).toBe('ProSupport')
expect(meta[1].text).toMatch(/^Ends /) // date formatted + labelled
expect(meta[2]).toEqual({ text: 'Tag ABC123', mono: true })
})
it('drops empty meta values', () => {
const item = { vendor: 'HP', status: 'expired', statuscolor: '#F44336' }
const meta = mapMeta(warrantyPanel, { ...item })
expect(meta).toEqual([]) // no servicelevel/enddate/servicetag
expect(mapBadge(warrantyPanel, item).label).toBe('Expired')
})
it('builds the manage link with the assetid', () => {
expect(manageLink(warrantyPanel, 7)).toBe('/warranties?addfor=7')
})
it('is visible when it has rows or an empty message', () => {
expect(panelVisible(warrantyPanel)).toBe(true)
expect(panelVisible({ ...warrantyPanel, _data: [] })).toBe(true) // has empty
expect(panelVisible({ render: 'list', _data: [] })).toBe(false)
})
})
describe('expandable row body (activity feed)', () => {
const activityPanel = {
id: 'endpoints-activity', render: 'list',
map: { title: 'title', body: 'body' },
}
it('returns the body only for rows that carry one', () => {
expect(mapBody(activityPanel, { title: 'Crash', body: 'Faulting module' }))
.toBe('Faulting module')
expect(mapBody(activityPanel, { title: 'Crash' })).toBeNull()
expect(mapBody(activityPanel, { title: 'Crash', body: '' })).toBeNull()
})
it('returns null when the panel declares no body key', () => {
expect(mapBody(warrantyPanel, { body: 'ignored' })).toBeNull()
})
})
describe('list row filtering (tracked services vs processes)', () => {
const feedPanel = {
id: 'endpoints-tracked-services', render: 'list',
filter: { key: 'kind', equals: 'service' },
map: { title: 'name' },
_data: [
{ name: 'UDC MTConnect Agent', kind: 'service' },
{ name: 'DncMain', kind: 'process' },
{ name: 'Spooler', kind: 'SERVICE' },
],
}
it('keeps only rows whose filter key matches, case-insensitively', () => {
const kept = rows(feedPanel).map((r) => r.name)
expect(kept).toEqual(['UDC MTConnect Agent', 'Spooler'])
})
it('filters the same feed to processes when equals is process', () => {
const kept = rows({ ...feedPanel, filter: { key: 'kind', equals: 'process' } })
expect(kept.map((r) => r.name)).toEqual(['DncMain'])
})
it('returns every row when no filter is declared', () => {
const kept = rows({ ...feedPanel, filter: undefined })
expect(kept).toHaveLength(3)
})
})
describe('keyvalue + table + badge', () => {
it('keyvalue reads a fields array or a plain object', () => {
const fromFields = keyvalueFields({
render: 'keyvalue', _data: { fields: [{ label: 'Cal due', value: '2026-01-01', format: 'date' }] },
})
expect(fromFields[0].label).toBe('Cal due')
const fromObject = keyvalueFields({ render: 'keyvalue', _data: { hostname: 'PC1', online: true } })
expect(fromObject).toEqual([
{ label: 'Hostname', value: 'PC1' },
{ label: 'Online', value: 'Yes' },
])
})
it('table uses declared columns, else infers from row keys', () => {
const declared = tableColumns({ columns: [{ key: 'a', label: 'Alpha' }], _data: [{ a: 1 }] })
expect(declared).toEqual([{ key: 'a', label: 'Alpha' }])
const inferred = tableColumns({ _data: [{ vendor: 'x', model: 'y' }] })
expect(inferred.map((c) => c.label)).toEqual(['Vendor', 'Model'])
})
it('badge reads an array or a badges field', () => {
expect(badges({ render: 'badge', _data: [{ label: 'A', color: '#111' }] })).toEqual([{ label: 'A', color: '#111' }])
expect(badges({ render: 'badge', _data: { badges: [{ label: 'B', color: '#222' }] } })).toEqual([{ label: 'B', color: '#222' }])
})
})
describe('tabSections', () => {
const panel = {
render: 'tabs',
_data: {
sections: [
{ label: 'General', fields: [{ label: 'Cnc', value: 'OKUMA' }] },
{ label: 'eFocas', fields: [{ label: 'IpAddr', value: '192.168.1.1', mono: true }] },
// A section whose fields all dropped out must not leave an empty tab.
{ label: 'NTSHR', fields: [] },
],
},
}
it('returns one entry per non-empty section', () => {
expect(tabSections(panel).map((s) => s.label)).toEqual(['General', 'eFocas'])
})
it('formats field values and carries mono through', () => {
const efocas = tabSections(panel)[1]
expect(efocas.fields[0]).toEqual({ label: 'IpAddr', value: '192.168.1.1', mono: true })
})
it('is empty for a payload with no sections', () => {
expect(tabSections({ render: 'tabs', _data: null })).toEqual([])
})
it('drives panel visibility', () => {
expect(panelVisible(panel)).toBe(true)
expect(panelVisible({ render: 'tabs', _data: { sections: [] } })).toBe(false)
})
})
describe('rollupStatus', () => {
const status = {
key: 'statecolor',
levels: { green: 'ok', yellow: 'warning', red: 'critical' },
}
it('returns null when the panel declares no status', () => {
expect(rollupStatus({ _data: [{ statecolor: 'green' }] })).toBeNull()
})
it('returns null when there are no rows', () => {
expect(rollupStatus({ status, _data: [] })).toBeNull()
})
it('is ok (green) when every row is ok', () => {
const result = rollupStatus({ status, _data: [{ statecolor: 'green' }, { statecolor: 'green' }] })
expect(result).toEqual({ level: 'ok', color: '#2dce89' })
})
it('is warning (yellow) when a warning is present but no critical', () => {
const result = rollupStatus({ status, _data: [{ statecolor: 'green' }, { statecolor: 'yellow' }] })
expect(result).toEqual({ level: 'warning', color: '#ffc107' })
})
it('is critical (red) when any row is stopped', () => {
const result = rollupStatus({ status, _data: [{ statecolor: 'yellow' }, { statecolor: 'red' }] })
expect(result).toEqual({ level: 'critical', color: '#f5365c' })
})
it('ignores unmapped values without raising the level', () => {
const result = rollupStatus({ status, _data: [{ statecolor: 'gray' }, { statecolor: 'green' }] })
expect(result).toEqual({ level: 'ok', color: '#2dce89' })
})
it('returns null when no row value maps to a known level', () => {
expect(rollupStatus({ status, _data: [{ statecolor: 'gray' }] })).toBeNull()
})
})
describe('rowAction (inline MTConnect toggle)', () => {
const panel = { id: 'endpoints-services', render: 'list', commandendpoint: '/api/endpoints/5/commands' }
it('returns null for rows without an action', () => {
expect(rowAction(panel, { name: 'Some Service' })).toBeNull()
expect(rowAction(panel, { name: 'x', action: { status: 'mtconnect_status' } })).toBeNull()
})
it('offers Stop when the row is running', () => {
const info = rowAction(panel, {
name: 'MTConnect',
action: { running: true, start: 'mtconnect_start', stop: 'mtconnect_stop', status: 'mtconnect_status' },
})
expect(info.running).toBe(true)
expect(info.toggle).toBe('mtconnect_stop')
expect(info.toggleLabel).toBe('Stop')
expect(info.endpoint).toBe('/api/endpoints/5/commands')
})
it('offers Start when the row is not running', () => {
const info = rowAction(panel, {
name: 'MTConnect',
action: { running: false, start: 'mtconnect_start', stop: 'mtconnect_stop', status: 'mtconnect_status' },
})
expect(info.running).toBe(false)
expect(info.toggle).toBe('mtconnect_start')
expect(info.toggleLabel).toBe('Start')
})
})
describe('facet chips on a card that shows a health dot', () => {
// The case that prompted this: the Activity card shows a RED dot because a
// critical event is in the data, while the visible rows are all info because
// the critical is older and sits further down. The dot and the list appear to
// disagree. Chips resolve it: the count sits under the dot, and one click
// brings the critical rows to the surface.
const activity = {
id: 'endpoints-activity',
render: 'list',
status: {
key: 'severity',
levels: { info: 'ok', warning: 'warning', critical: 'critical' },
},
_data: [
{ title: 'Successful logon', severity: 'info' },
{ title: 'Successful logon', severity: 'info' },
{ title: 'Scheduled task created', severity: 'warning' },
{ title: 'Unexpected shutdown', severity: 'critical' },
],
}
it('derives chips from the status declaration, so no panel declares filters twice', () => {
const chips = facets(activity)
expect(chips.map((c) => c.value)).toEqual(['critical', 'warning', 'info'])
})
it('orders worst first rather than alphabetically', () => {
expect(facets(activity)[0].value).toBe('critical')
})
it('counts from the same rows the dot is computed from, so they cannot disagree', () => {
const chips = facets(activity)
expect(chips.find((c) => c.value === 'critical').count).toBe(1)
expect(chips.find((c) => c.value === 'info').count).toBe(2)
expect(rollupStatus(activity).level).toBe('critical')
})
it('labels a colour-keyed panel by its level, not by the colour name', () => {
const services = {
id: 'svc',
render: 'list',
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
_data: [{ statecolor: 'green' }, { statecolor: 'red' }],
}
const labels = facets(services).map((c) => c.label)
expect(labels).toContain('Critical')
expect(labels).not.toContain('Red')
})
it('shows everything when nothing is selected, rather than hiding the card', () => {
expect(facetedRows(activity, []).length).toBe(4)
expect(facetedRows(activity, undefined).length).toBe(4)
})
it('filters to the selected severities, and allows more than one', () => {
expect(facetedRows(activity, ['critical']).map((r) => r.title))
.toEqual(['Unexpected shutdown'])
expect(facetedRows(activity, ['critical', 'warning']).length).toBe(2)
})
it('offers no chips for a panel with no status rollup', () => {
expect(facets({ id: 'x', render: 'list', _data: [{ a: 1 }] })).toEqual([])
})
it('prefers an explicit facets declaration over the status rollup', () => {
// Services carry BOTH: a statecolor status dot and a statelabel facet.
// Chips must follow the declaration, because a service is Running or
// Stopped - "OK" and "Critical" is not how anybody describes one.
const services = {
id: 'endpoints-services',
render: 'list',
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
facets: { key: 'statelabel', order: ['Running', 'Stopped', 'Paused'] },
_data: [
{ statelabel: 'Running', statecolor: 'green' },
{ statelabel: 'Stopped', statecolor: 'red' },
{ statelabel: 'Paused', statecolor: 'red' },
],
}
const chips = facets(services)
expect(chips.map((c) => c.label)).toEqual(['Running', 'Stopped', 'Paused'])
expect(chips.map((c) => c.label)).not.toContain('Critical')
})
it('gives an unlisted state its own chip rather than dropping the row', () => {
// Windows has transitional states, and an agent can report something the
// order list never anticipated. Those rows must stay reachable.
const services = {
id: 'svc2',
render: 'list',
facets: { key: 'statelabel', order: ['Running', 'Stopped'] },
_data: [{ statelabel: 'Running' }, { statelabel: 'Start Pending' }],
}
const chips = facets(services)
expect(chips.map((c) => c.value)).toEqual(['Running', 'Start Pending'])
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)
})
})