Fleet console: triage band, activity feed, live state first
This commit is contained in:
185
frontend/src/components/AssetTriageBand.vue
Normal file
185
frontend/src/components/AssetTriageBand.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div v-if="hasAnything" class="triage-band">
|
||||
<div v-if="liveness" class="triage-item">
|
||||
<span class="triage-label">Status</span>
|
||||
<span class="triage-chip" :class="liveness.level">
|
||||
<span class="triage-dot"></span>{{ liveness.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="machines.length" class="triage-item">
|
||||
<span class="triage-label">{{ machines.length === 1 ? 'Serves machine' : 'Serves machines' }}</span>
|
||||
<span class="triage-values">
|
||||
<router-link
|
||||
v-for="machine in machines"
|
||||
:key="machine.assetid"
|
||||
class="triage-chip link"
|
||||
:to="`/assets/${machine.assetid}`"
|
||||
>{{ machine.assetnumber || machine.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="accessmethods && accessmethods.length" class="triage-item">
|
||||
<span class="triage-label">Remote access</span>
|
||||
<span class="triage-values">
|
||||
<a
|
||||
v-for="method in accessmethods"
|
||||
:key="method.name || method.protocol"
|
||||
class="triage-chip link"
|
||||
:class="{ disabled: !method.link }"
|
||||
:href="method.link || undefined"
|
||||
>{{ method.name || method.protocol }}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="locationname" class="triage-item">
|
||||
<span class="triage-label">Location</span>
|
||||
<span class="triage-value">{{ locationname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* The triage band: what somebody needs at 6am with a machine down.
|
||||
*
|
||||
* Core-owned and shared on purpose. The five asset detail pages each rebuild
|
||||
* their own hero and grid, which is how they drifted apart; anything new should
|
||||
* be written once so machines, printers and network devices can adopt it
|
||||
* without a fifth copy.
|
||||
*
|
||||
* "Serves machine" is the reason this exists. On a shop floor a PC matters
|
||||
* because a machine stops without it, and that fact was buried in the generic
|
||||
* relationships card near the bottom of the page.
|
||||
*/
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
assetid: { type: Number, required: true },
|
||||
// Optional because liveness is contributed, not core: only a plugin that
|
||||
// collects telemetry can answer it, and a site may not have one installed.
|
||||
// Shape: { level: 'ok'|'warning'|'critical'|'unknown', label: 'Reporting, 4m ago' }
|
||||
liveness: { type: Object, default: null },
|
||||
accessmethods: { type: Array, default: () => [] },
|
||||
locationname: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const machines = ref([])
|
||||
|
||||
const hasAnything = computed(() =>
|
||||
Boolean(props.liveness) ||
|
||||
machines.value.length > 0 ||
|
||||
(props.accessmethods && props.accessmethods.length > 0) ||
|
||||
Boolean(props.locationname)
|
||||
)
|
||||
|
||||
onMounted(loadMachines)
|
||||
watch(() => props.assetid, loadMachines)
|
||||
|
||||
async function loadMachines() {
|
||||
machines.value = []
|
||||
if (!props.assetid) return
|
||||
try {
|
||||
const response = await api.get(`/assets/${props.assetid}/relationships`)
|
||||
const data = response.data.data || {}
|
||||
const found = []
|
||||
// BOTH directions. The canonical edge is PC controls machine, but
|
||||
// production also holds reciprocal rows, so reading one direction shows
|
||||
// the link on half the pages and not the other half.
|
||||
for (const edge of data.outgoing || []) collect(edge, edge.targetasset, found)
|
||||
for (const edge of data.incoming || []) collect(edge, edge.sourceasset, found)
|
||||
machines.value = found
|
||||
} catch (error) {
|
||||
// A band that cannot load its links simply does not show them. It sits
|
||||
// above the fold on a page whose other cards are fine.
|
||||
machines.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function collect(edge, other, found) {
|
||||
if (!other) return
|
||||
if ((edge.relationshiptypename || '').toLowerCase() !== 'controls') return
|
||||
if ((other.assettypename || '').toLowerCase() !== 'machine') return
|
||||
if (found.some(m => m.assetid === other.assetid)) return
|
||||
found.push({ assetid: other.assetid, assetnumber: other.assetnumber, name: other.name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.triage-band {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem 2rem;
|
||||
padding: 0.9rem 0 0;
|
||||
margin-top: 0.9rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.triage-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.triage-label {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.triage-values {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.triage-value { font-size: 0.95rem; }
|
||||
|
||||
.triage-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.15rem 0.6rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.triage-chip.link { border-color: var(--primary); color: var(--primary); }
|
||||
.triage-chip.link:hover { background: var(--bg-card); }
|
||||
|
||||
.triage-chip.disabled {
|
||||
border-color: var(--border);
|
||||
color: var(--text-light);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.triage-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.4rem;
|
||||
background: var(--text-light);
|
||||
}
|
||||
|
||||
.triage-chip.ok .triage-dot { background: var(--success); }
|
||||
.triage-chip.warning .triage-dot { background: var(--warning); }
|
||||
.triage-chip.critical .triage-dot { background: var(--danger); }
|
||||
|
||||
.triage-chip.ok { color: var(--success-dark); }
|
||||
.triage-chip.warning { color: var(--warning-dark); }
|
||||
.triage-chip.critical { color: var(--danger); }
|
||||
|
||||
/* Dark mode redefines --success and --warning to bright variants but leaves
|
||||
the -dark ones tuned for a white background, so they read as dark green and
|
||||
dark amber on the dark card. Scoped to the stamped attribute, matching
|
||||
style.css: a bare prefers-color-scheme query leaks into light mode on a
|
||||
dark-OS machine. */
|
||||
[data-theme="dark"] .triage-chip.ok { color: var(--success); }
|
||||
[data-theme="dark"] .triage-chip.warning { color: var(--warning); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user