Fleet console gap closures: critical tile, filters, policy card, action placement

This commit is contained in:
2026-08-28 23:00:37 -04:00
parent 7e779a6829
commit 09d06399d3
7 changed files with 235 additions and 8 deletions

View File

@@ -7,7 +7,7 @@
<!-- Summary strip. Wired to dashboard routes that already existed and
nothing called; each tile is a filter shortcut, not just a number. -->
<div v-if="summary" class="fleet-tiles">
<button class="fleet-tile" :class="{ on: status === '' }" @click="setStatus('')">
<button class="fleet-tile" :class="{ on: status === '' && !problemsonly }" @click="clearFilters">
<span class="n">{{ summary.total }}</span><span class="l">Endpoints</span>
</button>
<button class="fleet-tile ok" :class="{ on: status === 'reporting' }" @click="setStatus('reporting')">
@@ -19,6 +19,11 @@
<button class="fleet-tile bad" :class="{ on: status === 'never' }" @click="setStatus('never')">
<span class="n">{{ summary.never }}</span><span class="l">Never reported</span>
</button>
<!-- Crash, shutdown and corruption events in the stability window. The
route existed and nothing called it. -->
<button class="fleet-tile bad" :class="{ on: problemsonly }" @click="toggleProblems">
<span class="n">{{ criticalcount }}</span><span class="l">With critical events</span>
</button>
</div>
<!-- Filters -->
@@ -36,6 +41,18 @@
<option value="quiet">Quiet</option>
<option value="never">Never reported</option>
</select>
<select v-model="computertypeid" class="form-control fleet-filter" @change="applyFilters">
<option value="">All PC types</option>
<option v-for="type in computertypes" :key="type.computertypeid" :value="type.computertypeid">
{{ type.computertype }}
</option>
</select>
<select v-model="locationid" class="form-control fleet-filter" @change="applyFilters">
<option value="">All locations</option>
<option v-for="place in locations" :key="place.locationid" :value="place.locationid">
{{ place.locationname }}
</option>
</select>
<select v-model="sort" class="form-control fleet-filter" @change="applyFilters">
<option value="status">Sort: worst first</option>
<option value="hostname">Sort: hostname</option>
@@ -82,7 +99,7 @@
<!-- What stops if this PC is dead. On a shop floor that is
the first thing anyone needs to know. -->
<span v-if="!item.machines || !item.machines.length">-</span>
<span v-else>{{ item.machines.map(m => m.assetnumber).join(', ') }}</span>
<span v-else>{{ item.machines.map(machineLabel).join(', ') }}</span>
</td>
<td>{{ item.computertype || '-' }}</td>
<td>
@@ -119,7 +136,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { endpointsApi } from '@/api'
import { endpointsApi, computersApi, locationsApi } from '@/api'
import PaginationBar from '@/components/PaginationBar.vue'
import { useListQuery } from '@/composables/listQuery'
@@ -140,18 +157,42 @@ const status = ref('')
// endpoints to the top and buries the ones somebody needs to act on.
const sort = ref('status')
const summary = ref(null)
const criticalcount = ref(0)
const computertypeid = ref('')
const locationid = ref('')
const problemsonly = ref(false)
const computertypes = ref([])
const locations = ref([])
let searchTimeout = null
onMounted(() => {
loadEndpoints()
loadSummary()
loadFilterOptions()
})
async function loadFilterOptions() {
// listAll, not list: perpage is clamped to 100 server-side, so a long type or
// location list would silently lose its tail in a dropdown.
try {
computertypes.value = await computersApi.types.listAll()
} catch (error) {
computertypes.value = []
}
try {
locations.value = await locationsApi.listAll()
} catch (error) {
locations.value = []
}
}
async function loadSummary() {
try {
const response = await endpointsApi.dashboardSummary()
summary.value = response.data.data || null
const stability = await endpointsApi.dashboardStability()
criticalcount.value = (stability.data.data || []).length
} catch (error) {
// The strip is a convenience, not the page. If it fails the table still
// works, so say nothing rather than blocking on it.
@@ -169,10 +210,15 @@ async function loadEndpoints() {
if (search.value) params.search = search.value
if (status.value) params.status = status.value
if (sort.value) params.sort = sort.value
if (computertypeid.value) params.computertypeid = computertypeid.value
if (locationid.value) params.locationid = locationid.value
loadError.value = ''
const response = await endpointsApi.list(params)
endpoints.value = response.data.data || []
const rows = response.data.data || []
endpoints.value = problemsonly.value
? rows.filter(row => row.health && row.health.level !== 'ok')
: rows
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (error) {
console.error('Error loading endpoints:', error)
@@ -217,9 +263,33 @@ function formatRelative(value) {
function setStatus(value) {
status.value = value
problemsonly.value = false
applyFilters()
}
function clearFilters() {
status.value = ''
problemsonly.value = false
applyFilters()
}
// Client-side, unlike the other filters: "has a problem" is the health rollup,
// which is assembled per row after pagination rather than in the query. Sorting
// already puts trouble first, so this narrows the page rather than the fleet -
// worth knowing before someone reads the count as fleet-wide.
function toggleProblems() {
problemsonly.value = !problemsonly.value
status.value = ''
applyFilters()
}
function machineLabel(machine) {
if (machine.name && machine.assetnumber && machine.name !== machine.assetnumber) {
return `${machine.assetnumber} ${machine.name}`
}
return machine.assetnumber || machine.name || ''
}
function applyFilters() {
setPage(1)
loadEndpoints()