fc3i(ui): QueuesTable.vue — shared queue/worker/recent table (compact + detailed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<v-table density="compact" class="fc-queues-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Queue</th>
|
||||
<th class="text-right">Depth</th>
|
||||
<th class="text-right">Workers</th>
|
||||
<th v-if="!compact" class="text-right">Active</th>
|
||||
<th class="text-right">Last min: ok / err</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="q in QUEUE_NAMES" :key="q">
|
||||
<td>{{ q }}</td>
|
||||
<td class="text-right fc-tabular" :class="depthClass(q)">
|
||||
{{ formatDepth(q) }}
|
||||
</td>
|
||||
<td class="text-right fc-tabular">{{ workerCount(q) }}</td>
|
||||
<td v-if="!compact" class="text-right fc-tabular">{{ activeCount(q) }}</td>
|
||||
<td class="text-right fc-tabular">
|
||||
<span class="fc-ok">{{ recentOk(q) }}</span>
|
||||
<span class="fc-sep">/</span>
|
||||
<span :class="recentErr(q) > 0 ? 'fc-err' : 'fc-muted'">
|
||||
{{ recentErr(q) }}<span v-if="recentErr(q) > 0"> ⚠</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
queues: { type: Object, default: null }, // store.queues
|
||||
workers: { type: Object, default: null }, // store.workers
|
||||
recentMinute: { type: Array, default: () => [] }, // store.recentMinute
|
||||
compact: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const QUEUE_NAMES = [
|
||||
'default', 'import', 'thumbnail', 'ml',
|
||||
'download', 'scan', 'maintenance',
|
||||
]
|
||||
|
||||
function formatDepth(name) {
|
||||
const d = props.queues?.queues?.[name]
|
||||
if (d == null) return '—'
|
||||
return d.toLocaleString()
|
||||
}
|
||||
|
||||
function depthClass(name) {
|
||||
const d = props.queues?.queues?.[name]
|
||||
const workers = workerCount(name)
|
||||
// Queue has depth but no workers → warning.
|
||||
if (d != null && d > 0 && workers === 0) return 'fc-warn'
|
||||
return ''
|
||||
}
|
||||
|
||||
function workerCount(name) {
|
||||
if (!props.workers?.workers) return 0
|
||||
let count = 0
|
||||
for (const info of Object.values(props.workers.workers)) {
|
||||
if (info.queues?.includes(name)) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function activeCount(name) {
|
||||
if (!props.workers?.workers) return 0
|
||||
// Active count is per-worker, not per-queue. Approximate: sum of
|
||||
// active_count across workers that subscribe to this queue.
|
||||
let count = 0
|
||||
for (const info of Object.values(props.workers.workers)) {
|
||||
if (info.queues?.includes(name)) count += info.active_count || 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
const recentByQueue = computed(() => {
|
||||
const out = {}
|
||||
for (const r of props.recentMinute) {
|
||||
if (!out[r.queue]) out[r.queue] = { ok: 0, err: 0 }
|
||||
if (r.status === 'ok') out[r.queue].ok++
|
||||
else if (r.status === 'error' || r.status === 'timeout') out[r.queue].err++
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
function recentOk(name) { return recentByQueue.value[name]?.ok || 0 }
|
||||
function recentErr(name) { return recentByQueue.value[name]?.err || 0 }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-queues-table { background: transparent; }
|
||||
.fc-tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
.fc-ok { color: rgb(var(--v-theme-success, 76 175 80)); }
|
||||
.fc-err { color: rgb(var(--v-theme-error, 220 80 80)); }
|
||||
.fc-warn { color: rgb(var(--v-theme-warning, 255 179 0)); font-weight: 500; }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
.fc-sep {
|
||||
margin: 0 4px;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user