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.
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
// Pure render helpers for PluginAssetPanels.vue (ADR-010 get_asset_panels, Path
|
|
// A). Kept separate from the .vue so the mapping logic - which turns a plugin's
|
|
// JSON panel declaration + its endpoint data into rendered rows - is unit
|
|
// tested directly (same pattern as geenforce/entryForm.js).
|
|
|
|
export function subst(text, assetid) {
|
|
return String(text).replace('{assetid}', assetid)
|
|
}
|
|
|
|
// Panel endpoints are declared absolute (/api/...); the api instance already
|
|
// carries the /api base, so strip it before fetching through the instance.
|
|
export function toApiPath(endpoint, assetid) {
|
|
return subst(endpoint, assetid).replace(/^\/api(?=\/)/, '')
|
|
}
|
|
|
|
export function formatValue(value, spec = {}) {
|
|
if (value === null || value === undefined || value === '') return ''
|
|
if (spec.format === 'date') {
|
|
const raw = String(value)
|
|
return new Date(raw + (raw.length === 10 ? 'T00:00:00' : '')).toLocaleDateString()
|
|
}
|
|
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
return value
|
|
}
|
|
|
|
export function humanize(key) {
|
|
return key.charAt(0).toUpperCase() + key.slice(1)
|
|
}
|
|
|
|
export function rows(panel) {
|
|
const data = panel._data
|
|
if (Array.isArray(data)) return data
|
|
if (data && Array.isArray(data.rows)) return data.rows
|
|
if (data && Array.isArray(data.items)) return data.items
|
|
return []
|
|
}
|
|
|
|
export function mapTitle(panel, item) {
|
|
const key = panel.map && panel.map.title
|
|
return key ? item[key] : ''
|
|
}
|
|
|
|
export function mapBadge(panel, item) {
|
|
const badge = panel.map && panel.map.badge
|
|
if (!badge) return null
|
|
const raw = item[badge.label]
|
|
const label = (badge.labelmap && badge.labelmap[raw]) || raw
|
|
return { label, color: badge.color ? item[badge.color] : undefined }
|
|
}
|
|
|
|
export function mapMeta(panel, item) {
|
|
const meta = (panel.map && panel.map.meta) || []
|
|
return meta
|
|
.map((spec) => {
|
|
const value = formatValue(item[spec.key], spec)
|
|
if (value === '') return null
|
|
const text = spec.label ? `${spec.label} ${value}` : value
|
|
return { text, mono: !!spec.mono }
|
|
})
|
|
.filter(Boolean)
|
|
}
|
|
|
|
export function manageLink(panel, assetid) {
|
|
return panel.manage ? subst(panel.manage.to, assetid) : null
|
|
}
|
|
|
|
export function keyvalueFields(panel) {
|
|
const data = panel._data
|
|
if (data && Array.isArray(data.fields)) {
|
|
return data.fields.map((f) => ({
|
|
label: f.label, value: formatValue(f.value, f), mono: !!f.mono,
|
|
}))
|
|
}
|
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
return Object.entries(data).map(([k, v]) => ({
|
|
label: humanize(k), value: formatValue(v),
|
|
}))
|
|
}
|
|
return []
|
|
}
|
|
|
|
export function tableColumns(panel) {
|
|
if (Array.isArray(panel.columns) && panel.columns.length) {
|
|
return panel.columns.map((c) => ({ key: c.key, label: c.label || humanize(c.key) }))
|
|
}
|
|
const first = rows(panel)[0]
|
|
if (!first) return []
|
|
return Object.keys(first).map((k) => ({ key: k, label: humanize(k) }))
|
|
}
|
|
|
|
export function cell(row, col) {
|
|
return formatValue(row[col.key], col)
|
|
}
|
|
|
|
export function badges(panel) {
|
|
const data = panel._data
|
|
const list = Array.isArray(data) ? data : (data && data.badges) || []
|
|
return list.map((b) => ({ label: b.label, color: b.color }))
|
|
}
|
|
|
|
export function panelVisible(panel) {
|
|
if (panel.render === 'list') return rows(panel).length > 0 || !!panel.empty
|
|
if (panel.render === 'keyvalue') return keyvalueFields(panel).length > 0
|
|
if (panel.render === 'table') return rows(panel).length > 0
|
|
if (panel.render === 'badge') return badges(panel).length > 0
|
|
return false
|
|
}
|