harden CSV import reconciliation and preserve saved model selections
Default fuzzy reconcile threshold to 0 and refuse merges when digit runs differ so numbered labels like Bay 311 never bind to Bay 310. Keep edit forms from clearing saved model fields when category filtering hides untyped rows. Includes in-progress computers policy and endpoints command feature work.
This commit is contained in:
@@ -4,7 +4,30 @@
|
||||
on any asset detail page, so a plugin adds detail-page UI with no Vue. -->
|
||||
<template v-for="panel in panels" :key="panel.id">
|
||||
<div v-if="panelVisible(panel)" class="section-card">
|
||||
<h3 class="section-title">{{ panel.title }}</h3>
|
||||
<!-- collapsible panels get a clickable header with a caret and, when the
|
||||
panel declares a status rollup, a green/yellow/red dot for the worst
|
||||
row (a stopped service or a warning/critical event). --><h3
|
||||
v-if="panel.collapsible"
|
||||
class="section-title pap-header"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-expanded="!isCollapsed(panel)"
|
||||
@click="toggleCollapse(panel)"
|
||||
@keydown.enter.prevent="toggleCollapse(panel)"
|
||||
@keydown.space.prevent="toggleCollapse(panel)"
|
||||
>
|
||||
<span class="pap-caret" :class="{ collapsed: isCollapsed(panel) }" aria-hidden="true">▾</span>
|
||||
<span class="pap-header-title">{{ panel.title }}</span>
|
||||
<span
|
||||
v-if="rollupStatus(panel)"
|
||||
class="pap-status-dot"
|
||||
:style="{ backgroundColor: rollupStatus(panel).color }"
|
||||
:title="rollupStatus(panel).level"
|
||||
></span>
|
||||
</h3>
|
||||
<h3 v-else class="section-title">{{ panel.title }}</h3>
|
||||
|
||||
<div v-show="!panel.collapsible || !isCollapsed(panel)" class="pap-body">
|
||||
|
||||
<!-- list: items each with a title, optional status badge, meta lines -->
|
||||
<div v-if="panel.render === 'list'" class="pap-list">
|
||||
@@ -16,6 +39,13 @@
|
||||
:style="colorStyle(mapBadge(panel, item).color)">
|
||||
{{ mapBadge(panel, item).label }}
|
||||
</span>
|
||||
<button
|
||||
v-if="rowAction(panel, item)"
|
||||
type="button"
|
||||
class="btn btn-sm pap-row-action"
|
||||
:disabled="actionBusy[panel.id]"
|
||||
@click="runRowAction(panel, item)"
|
||||
>{{ rowAction(panel, item).toggleLabel }}</button>
|
||||
</div>
|
||||
<div v-if="mapMeta(panel, item).length" class="pap-meta">
|
||||
<span v-for="(m, mi) in mapMeta(panel, item)" :key="mi"
|
||||
@@ -25,6 +55,9 @@
|
||||
<router-link v-if="panel.manage" :to="manageLink(panel, assetid)" class="pap-manage">
|
||||
{{ panel.manage.label || 'Manage' }}
|
||||
</router-link>
|
||||
<div v-if="actionError[panel.id]" class="pap-action-error">
|
||||
{{ actionError[panel.id] }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="pap-empty">
|
||||
<span class="muted">{{ panel.empty || 'Nothing to show.' }}</span>
|
||||
@@ -90,6 +123,7 @@
|
||||
<span v-for="(b, bi) in badges(panel)" :key="bi" class="status-badge"
|
||||
:style="colorStyle(b.color)">{{ b.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -100,7 +134,8 @@ import api from '../api'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import {
|
||||
toApiPath, rows, panelVisible, mapTitle, mapBadge, mapMeta, manageLink,
|
||||
keyvalueFields, tableColumns, cell, badges, tabSections,
|
||||
keyvalueFields, tableColumns, cell, badges, tabSections, rollupStatus,
|
||||
rowAction,
|
||||
} from './pluginAssetPanels'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -115,6 +150,45 @@ const tabs = ref({})
|
||||
function activeTab(panel) { return tabs.value[panel.id] || 0 }
|
||||
function setTab(panel, index) { tabs.value = { ...tabs.value, [panel.id]: index } }
|
||||
|
||||
// Collapsed state per panel id (default expanded), kept here for the same
|
||||
// reason as tabs: panels are replaced wholesale on refetch.
|
||||
const collapsed = ref({})
|
||||
function isCollapsed(panel) { return !!collapsed.value[panel.id] }
|
||||
function toggleCollapse(panel) {
|
||||
collapsed.value = { ...collapsed.value, [panel.id]: !collapsed.value[panel.id] }
|
||||
}
|
||||
|
||||
// Per-panel action state: busy guards a double-click while an enqueue POST is
|
||||
// in flight, and the error string surfaces a rejected enqueue (bad action, or
|
||||
// a 409 when a command is already queued). Keyed by panel id for the same
|
||||
// reason as tabs/collapsed: panels are replaced wholesale on refetch.
|
||||
const actionBusy = ref({})
|
||||
const actionError = ref({})
|
||||
|
||||
// Enqueue the inline row toggle's command (Start/Stop for an actionable
|
||||
// services/processes row, e.g. MTConnect), then reload so the row reflects the
|
||||
// new state. The agent long-polls, runs it, posts the result, and wakes an
|
||||
// immediate telemetry re-collect, so the settled run state lands on the next
|
||||
// panel refresh rather than after the full cadence.
|
||||
async function runRowAction(panel, item) {
|
||||
if (actionBusy.value[panel.id]) return
|
||||
const info = rowAction(panel, item)
|
||||
if (!info || !info.endpoint || !info.toggle) return
|
||||
actionBusy.value = { ...actionBusy.value, [panel.id]: true }
|
||||
actionError.value = { ...actionError.value, [panel.id]: '' }
|
||||
try {
|
||||
const body = { action: info.toggle }
|
||||
if (info.target) body.target = info.target
|
||||
await api.post(toApiPath(info.endpoint, props.assetid), body)
|
||||
await load()
|
||||
} catch (err) {
|
||||
const message = err?.response?.data?.message || 'Command failed to queue.'
|
||||
actionError.value = { ...actionError.value, [panel.id]: message }
|
||||
} finally {
|
||||
actionBusy.value = { ...actionBusy.value, [panel.id]: false }
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the declared panels for this asset, then each panel's data endpoint.
|
||||
async function load() {
|
||||
panels.value = []
|
||||
@@ -195,6 +269,22 @@ watch(() => props.assetid, load, { immediate: true })
|
||||
.pap-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||
.pap-table th, .pap-table td { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); }
|
||||
.pap-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.pap-row-action { margin-left: 0.5rem; flex: 0 0 auto; }
|
||||
.pap-action-error { color: var(--danger, #f5365c); font-size: 0.82rem; }
|
||||
.pap-header {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
cursor: pointer; user-select: none;
|
||||
}
|
||||
.pap-header:hover .pap-header-title { color: var(--primary); }
|
||||
.pap-header-title { flex: 1 1 auto; }
|
||||
.pap-caret {
|
||||
font-size: 0.7rem; color: var(--text-light);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.pap-caret.collapsed { transform: rotate(-90deg); }
|
||||
.pap-status-dot {
|
||||
flex: 0 0 auto; width: 0.7rem; height: 0.7rem; border-radius: 50%;
|
||||
}
|
||||
.mono { font-family: monospace; }
|
||||
.muted { color: var(--text-light); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user