A command reaching 'succeeded' only proves the agent ran the action, not that it posted a new telemetry snapshot. Reloading panels on command success alone re-rendered stale service/process rows, so a stopped process could still show running right after Refresh Status. Add a lightweight telemetrymeta probe exposing lastreportedat and gate both the header Refresh Status action and inline row Start/Stop on that stamp advancing past a pre-action baseline. Polling is bounded so an offline or idle agent never hangs the button, and messaging distinguishes a confirmed refresh from a queued one. Bring the .NET endpoint agent source (service, tray, installer) into the repo so Cameron can merge and test, with an agent/.gitignore keeping bin/obj/publish out. Includes the DncMain launch-path fix, tray single-file host env scrub, and best-effort file logger.
190 lines
6.8 KiB
JavaScript
190 lines
6.8 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
|
|
let list
|
|
if (Array.isArray(data)) list = data
|
|
else if (data && Array.isArray(data.rows)) list = data.rows
|
|
else if (data && Array.isArray(data.items)) list = data.items
|
|
else return []
|
|
// Optional panel-level row filter so two panels can share one endpoint feed
|
|
// and each show a slice of it (for example Tracked Services vs Tracked
|
|
// Processes off the same services snapshot). Absent filter = all rows.
|
|
const filter = panel.filter
|
|
if (filter && filter.key) {
|
|
const want = String(filter.equals).toLowerCase()
|
|
return list.filter((item) => String(item[filter.key]).toLowerCase() === want)
|
|
}
|
|
return list
|
|
}
|
|
|
|
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 }))
|
|
}
|
|
|
|
// Header status rollup for a card. A panel opts in with
|
|
// status: { key, levels: { <rowvalue>: 'critical'|'warning'|'ok'|'unknown' } }.
|
|
// The card header then shows a single green/yellow/red dot reflecting the worst
|
|
// row: any critical -> red, else any warning -> yellow, else ok -> green. A
|
|
// value not in levels is treated as unknown and does not raise the level.
|
|
export const STATUS_RANK = { ok: 1, unknown: 1, warning: 2, critical: 3 }
|
|
export const STATUS_COLOR = {
|
|
ok: '#2dce89',
|
|
warning: '#ffc107',
|
|
critical: '#f5365c',
|
|
unknown: '#6c757d',
|
|
}
|
|
|
|
export function rollupStatus(panel) {
|
|
const status = panel && panel.status
|
|
if (!status || !status.key) return null
|
|
const items = rows(panel)
|
|
if (!items.length) return null
|
|
const levels = status.levels || {}
|
|
let worst = 'ok'
|
|
let sawKnown = false
|
|
for (const item of items) {
|
|
const level = levels[item[status.key]]
|
|
if (!level) continue
|
|
sawKnown = true
|
|
if (STATUS_RANK[level] > STATUS_RANK[worst]) worst = level
|
|
}
|
|
if (!sawKnown) return null
|
|
return { level: worst, color: STATUS_COLOR[worst] || STATUS_COLOR.unknown }
|
|
}
|
|
|
|
// tabs: [{ label, fields: [{label, value, mono}] }]. One section is shown at a
|
|
// time, so a panel with 30 values stays the height of its largest section
|
|
// instead of dragging one multicol column far past the other.
|
|
export function tabSections(panel) {
|
|
const data = panel._data
|
|
const sections = (data && Array.isArray(data.sections)) ? data.sections : []
|
|
return sections
|
|
.map((s) => ({
|
|
label: s.label,
|
|
fields: (s.fields || []).map((f) => ({
|
|
label: f.label, value: formatValue(f.value, f), mono: !!f.mono,
|
|
})),
|
|
}))
|
|
.filter((s) => s.fields.length > 0)
|
|
}
|
|
|
|
// Inline row action for a list panel. A monitored services/processes row may
|
|
// carry action: { running, start, stop, status } when it has a registered
|
|
// command (e.g. MTConnect). The card renders a single toggle on that row: when
|
|
// running it offers Stop, otherwise Start. Rows with no action are badge-only.
|
|
// commandendpoint on the panel is where the enqueue POST goes.
|
|
export function rowAction(panel, item) {
|
|
const action = item && item.action
|
|
if (!action || (!action.start && !action.stop)) return null
|
|
const running = action.running === true
|
|
return {
|
|
running,
|
|
// The action name to enqueue for the primary toggle button.
|
|
toggle: running ? action.stop : action.start,
|
|
toggleLabel: running ? 'Stop' : 'Start',
|
|
// status is a non-mutating probe, always available when declared.
|
|
status: action.status || null,
|
|
// Generic actions (service_*/process_*) carry the subject to act on; fixed
|
|
// actions (mtconnect_*) leave this null.
|
|
target: action.target || null,
|
|
// The declared enqueue endpoint template; the caller substitutes assetid.
|
|
endpoint: panel.commandendpoint || null,
|
|
}
|
|
}
|
|
|
|
export function panelVisible(panel) {
|
|
if (panel.render === 'tabs') return tabSections(panel).length > 0
|
|
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
|
|
}
|