Dashboard cards, backup last-seen
This commit is contained in:
137
frontend/src/components/dashboardCards.js
Normal file
137
frontend/src/components/dashboardCards.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// Pure render helpers for DashboardCards.vue - the fleet dashboard's generic
|
||||
// renderers. Same shape as pluginAssetPanels.js: the mapping logic that turns a
|
||||
// plugin's JSON card declaration plus its endpoint data into rendered rows
|
||||
// lives here so it can be unit tested without mounting a component.
|
||||
//
|
||||
// WHY GENERIC RENDERERS AT ALL: the older widget contract named a Vue component
|
||||
// per widget. That cannot survive a lean build, where a plugin's component may
|
||||
// never be staged into the frontend bundle - which is exactly why five plugins
|
||||
// declared widgets pointing at components nobody ever wrote. A plugin declares
|
||||
// data and shape; core owns the drawing.
|
||||
|
||||
export function toApiPath(endpoint) {
|
||||
// Cards declare absolute endpoints (/api/...); the api instance already
|
||||
// carries the /api base, so strip it before fetching through the instance.
|
||||
return String(endpoint).replace(/^\/api(?=\/)/, '')
|
||||
}
|
||||
|
||||
export function rows(card) {
|
||||
const data = card._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 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 mapTitle(card, item) {
|
||||
const key = card.map && card.map.title
|
||||
return key ? item[key] : ''
|
||||
}
|
||||
|
||||
export function mapDetail(card, item) {
|
||||
const key = card.map && card.map.detail
|
||||
return key ? item[key] : ''
|
||||
}
|
||||
|
||||
export function mapMeta(card, item) {
|
||||
const meta = (card.map && card.map.meta) || []
|
||||
return meta
|
||||
.map((spec) => {
|
||||
const value = formatValue(item[spec.key], spec)
|
||||
if (value === '') return null
|
||||
// label prefixes, suffix trails: 'quiet for 3 days' reads as a sentence,
|
||||
// where 'quiet for 3' reads as a truncation.
|
||||
const text = `${spec.label ? spec.label + ' ' : ''}${value}${spec.suffix || ''}`
|
||||
return { text, mono: !!spec.mono }
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
// A link template is '/pcs/{computerid}'. A row whose substitution value is
|
||||
// missing gets NO link rather than a link to '/pcs/undefined' - the row still
|
||||
// shows, because the finding is real even when the inventory is behind.
|
||||
export function mapLink(card, item) {
|
||||
const template = card.map && card.map.link
|
||||
if (!template) return null
|
||||
let missing = false
|
||||
const href = String(template).replace(/\{(\w+)\}/g, (_m, key) => {
|
||||
const value = item[key]
|
||||
if (value === null || value === undefined || value === '') {
|
||||
missing = true
|
||||
return ''
|
||||
}
|
||||
return value
|
||||
})
|
||||
return missing ? null : href
|
||||
}
|
||||
|
||||
export function cardRows(card) {
|
||||
return rows(card).map((item) => ({
|
||||
title: mapTitle(card, item),
|
||||
detail: mapDetail(card, item),
|
||||
meta: mapMeta(card, item),
|
||||
link: mapLink(card, item),
|
||||
timestamp: card.map && card.map.timestamp ? item[card.map.timestamp] : null,
|
||||
}))
|
||||
}
|
||||
|
||||
// A metric card is the count, for the cases where the count IS the story.
|
||||
export function metricValue(card) {
|
||||
const data = card._data
|
||||
if (typeof data === 'number') return data
|
||||
if (data && typeof data.value === 'number') return data.value
|
||||
return rows(card).length
|
||||
}
|
||||
|
||||
// EMPTY HANDLING is the part that keeps the board readable. A card reporting
|
||||
// "nothing wrong" every day teaches people to stop reading the page - the same
|
||||
// way a fleet log reached 3,234 lines of which 3,217 were one repeated line.
|
||||
// Default is to disappear; 'line' is for the few where absence is itself news.
|
||||
export function cardVisible(card) {
|
||||
if (card._data === null || card._data === undefined) return false // fetch failed
|
||||
const populated = card.render === 'metric'
|
||||
? metricValue(card) > 0
|
||||
: rows(card).length > 0
|
||||
if (populated) return true
|
||||
return card.empty === 'line'
|
||||
}
|
||||
|
||||
// Sorted by how much it wants a person: critical, then warning, then info, and
|
||||
// by declared position within a severity. Ordering by position alone would let
|
||||
// an info card sit above a failure.
|
||||
const SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 }
|
||||
|
||||
export function sortCards(cards) {
|
||||
return [...cards].sort((a, b) => {
|
||||
const sa = SEVERITY_ORDER[a.severity] ?? 3
|
||||
const sb = SEVERITY_ORDER[b.severity] ?? 3
|
||||
if (sa !== sb) return sa - sb
|
||||
return (a.position ?? 99) - (b.position ?? 99)
|
||||
})
|
||||
}
|
||||
|
||||
// A card declares the permission its endpoint requires. Filtering here keeps
|
||||
// the dashboard from becoming a way around RBAC, and avoids firing a request
|
||||
// that would only 403. `hasPermission` comes from the auth store.
|
||||
export function permittedCards(cards, hasPermission) {
|
||||
return cards.filter((card) => !card.permission || hasPermission(card.permission))
|
||||
}
|
||||
|
||||
// Only cards using a renderer core actually has. An unknown render mode is
|
||||
// skipped rather than drawn blank, so a plugin built against a newer core
|
||||
// degrades instead of leaving a hole on the page.
|
||||
export const RENDERERS = ['exceptions', 'metric', 'list']
|
||||
|
||||
export function renderableCards(cards) {
|
||||
return cards.filter((card) => RENDERERS.includes(card.render))
|
||||
}
|
||||
Reference in New Issue
Block a user