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.
This commit is contained in:
@@ -52,7 +52,20 @@
|
||||
<div v-for="(item, index) in visibleRows(panel)" :key="index" class="pap-item">
|
||||
<div class="pap-item-top">
|
||||
<span class="pap-title">{{ mapTitle(panel, item) }}</span>
|
||||
<span v-if="mapBadge(panel, item)" class="status-badge"
|
||||
<!-- On a row carrying a body the severity badge IS the expander:
|
||||
it already says what the row is, and a second control beside
|
||||
it only repeated that. Rows with no body keep the plain
|
||||
non-interactive badge. -->
|
||||
<button
|
||||
v-if="mapBadge(panel, item) && mapBody(panel, item)"
|
||||
type="button"
|
||||
class="status-badge pap-badge-toggle"
|
||||
:style="colorStyle(mapBadge(panel, item).color)"
|
||||
:aria-expanded="isBodyOpen(panel, index) ? 'true' : 'false'"
|
||||
:title="isBodyOpen(panel, index) ? 'Hide details' : 'Show details'"
|
||||
@click="toggleBody(panel, index)"
|
||||
>{{ mapBadge(panel, item).label }}</button>
|
||||
<span v-else-if="mapBadge(panel, item)" class="status-badge"
|
||||
:style="colorStyle(mapBadge(panel, item).color)">
|
||||
{{ mapBadge(panel, item).label }}
|
||||
</span>
|
||||
@@ -68,6 +81,9 @@
|
||||
<span v-for="(m, mi) in mapMeta(panel, item)" :key="mi"
|
||||
:class="{ mono: m.mono }">{{ m.text }}</span>
|
||||
</div>
|
||||
<div v-if="isBodyOpen(panel, index) && mapBody(panel, item)" class="pap-body">
|
||||
{{ mapBody(panel, item) }}
|
||||
</div>
|
||||
</div>
|
||||
<router-link v-if="panel.manage" :to="manageLink(panel, assetid)" class="pap-manage">
|
||||
{{ panel.manage.label || 'Manage' }}
|
||||
@@ -240,7 +256,7 @@ import { ref, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||
import api from '../api'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import {
|
||||
toApiPath, rows, panelVisible, mapTitle, mapBadge, mapMeta, manageLink,
|
||||
toApiPath, rows, panelVisible, mapTitle, mapBadge, mapMeta, mapBody, manageLink,
|
||||
keyvalueFields, tableColumns, cell, badges, tabSections, rollupStatus,
|
||||
rowAction, facets, facetedRows, formatValue,
|
||||
} from './pluginAssetPanels'
|
||||
@@ -365,6 +381,24 @@ function visibleRows(panel) {
|
||||
return facetedRows(panel, facetSelection.value[panel.id])
|
||||
}
|
||||
|
||||
// Which row bodies are expanded, keyed by panel id and row index. Closed on
|
||||
// every panel refresh: the row at index 3 after a refresh is not necessarily
|
||||
// the row that was opened, so keeping it open would expand an unrelated event.
|
||||
const bodyOpen = ref({})
|
||||
|
||||
function bodyKey(panel, index) {
|
||||
return `${panel.id}:${index}`
|
||||
}
|
||||
|
||||
function isBodyOpen(panel, index) {
|
||||
return !!bodyOpen.value[bodyKey(panel, index)]
|
||||
}
|
||||
|
||||
function toggleBody(panel, index) {
|
||||
const key = bodyKey(panel, index)
|
||||
bodyOpen.value = { ...bodyOpen.value, [key]: !bodyOpen.value[key] }
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
assetid: { type: [Number, String], default: null },
|
||||
// Which detail-page tab to render. Null renders every panel, which is what
|
||||
@@ -525,6 +559,7 @@ async function load() {
|
||||
panels.value = []
|
||||
revokeReportImageUrls()
|
||||
closeReport()
|
||||
bodyOpen.value = {}
|
||||
if (!props.assetid) return
|
||||
let declared
|
||||
try {
|
||||
@@ -562,6 +597,31 @@ defineExpose({ refresh: load, populatedGroups })
|
||||
.pap-title { font-weight: 600; color: var(--text); }
|
||||
.status-badge { padding: 0.15rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
||||
.pap-meta { margin-top: 0.35rem; display: flex; flex-wrap: wrap; gap: 0.75rem; font-size: 0.82rem; color: var(--text-light); }
|
||||
/* The badge doubles as the expander, so it needs to look pressable without
|
||||
becoming a second style of badge: same pill, just a pointer and a hover
|
||||
lift. margin-left:auto pins it right even on a row that also has an
|
||||
action button, where space-between alone would centre it. */
|
||||
.pap-badge-toggle {
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.pap-badge-toggle:hover { filter: brightness(1.1); }
|
||||
.pap-badge-toggle:focus-visible { outline: 2px solid var(--primary, #4181ff); outline-offset: 2px; }
|
||||
/* pre-wrap, not normal: the message keeps the line breaks the event was
|
||||
written with, which is what makes a multi-line fault readable. */
|
||||
.pap-body {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: var(--surface-2, rgba(127, 127, 127, 0.08));
|
||||
border-radius: 4px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-light);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.pap-empty { display: flex; align-items: center; gap: 0.6rem; }
|
||||
|
||||
.pap-facets {
|
||||
|
||||
@@ -138,6 +138,14 @@ export function mapMeta(panel, item) {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function mapBody(panel, item) {
|
||||
const key = panel.map && panel.map.body
|
||||
if (!key) return null
|
||||
const value = item[key]
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function manageLink(panel, assetid) {
|
||||
return panel.manage ? subst(panel.manage.to, assetid) : null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
toApiPath, subst, rows, mapTitle, mapBadge, mapMeta, manageLink,
|
||||
toApiPath, subst, rows, mapTitle, mapBadge, mapMeta, mapBody, manageLink,
|
||||
keyvalueFields, tableColumns, badges, panelVisible, tabSections, rollupStatus,
|
||||
rowAction, facets, facetedRows,
|
||||
} from './pluginAssetPanels'
|
||||
@@ -64,7 +64,6 @@ describe('list render mapping (warranty)', () => {
|
||||
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
|
||||
@@ -72,6 +71,24 @@ describe('list render mapping (warranty)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user