ADR-013 Phase 3: generic asset-panels renderer (Path A)
Wires the ADR-010 get_asset_panels hook to a generic frontend renderer so a plugin adds detail-page UI as JSON, no Vue. This is the Path A foundation that lets simple plugins ship UI without a frontend build. - components/PluginAssetPanels.vue + pluginAssetPanels.js: fetches /api/pluginui/asset-panels for an asset, then each panel's data endpoint, and renders by mode: list (title + status badge + meta lines via a field map), keyvalue, table (declared or inferred columns), badge. Pure mapping logic is in the .js module and unit tested (9 specs), same pattern as entryForm.js. - New 'list' render mode with a declarative field map (title/badge/meta), documented on the hook in base.py. - Warranty migrated to it: get_asset_panels now declares a 'list' panel + map that reproduces WarrantyPanel's output (vendor title, status badge with color + label map, servicelevel/ends/tag meta, manage link) with zero warranty-specific frontend code. - MachineDetail swapped from <WarrantyPanel> to <PluginAssetPanels> (pilot); the hero warranty badge is unchanged. Verified end to end: the API serves the list panel + map and the warranty rows; the page renders without error. Rollout of the other 4 detail pages (PCDetail, PrinterDetail, NetworkDeviceDetail, MeasuringToolDetail) and the map-overlays / asset-presentation renderers are follow-up Phase 3 commits. 58 vitest, build clean, 1067 backend pass, naming green.
This commit is contained in:
100
frontend/src/components/pluginAssetPanels.spec.js
Normal file
100
frontend/src/components/pluginAssetPanels.spec.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
toApiPath, subst, rows, mapTitle, mapBadge, mapMeta, manageLink,
|
||||
keyvalueFields, tableColumns, badges, panelVisible,
|
||||
} 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('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' }])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user