Files
FabledCurator/frontend/src/components/settings/BackupRunsTable.vue
T
bvandeusen eebc8e2413 refactor(dry-F2): centralize shared UI primitives (relative-time, toast, download-status)
- utils/date.js: add formatRelative(iso, {future,nullText}); migrate 6 sites
  (SourceRow, SubscriptionsTab, SourceHealthDot, SchedulerStatusBar +
  thin adapters in BackupRunsTable/SystemActivityTab for their '—' null text).
  PostCard (30d->absolute) and CredentialCard (mo/y buckets) intentionally
  keep bespoke formatters.
- utils/toast.js: toast(opts) wraps the globalThis.window?.__fcToast?.(...)
  incantation; migrate 63 call sites across 24 files.
- utils/downloadStatus.js: single source for the download-event status enum
  -> label/color/icon; collapse the 3 duplicate maps (DownloadStatChips,
  DownloadsFilterPopover, DownloadEventRow).

Net -33 lines. Platform metadata was already centralized in platformColor.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:20:07 -04:00

106 lines
3.3 KiB
Vue

<template>
<v-table density="compact" class="fc-backup-table">
<thead>
<tr>
<th>When</th>
<th>Status</th>
<th class="text-right">Duration</th>
<th class="text-right">Size</th>
<th>Tag</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="r in runs" :key="r.id">
<td class="fc-tabular" :title="r.started_at">
{{ formatRelative(r.started_at) }}
</td>
<td>
<v-icon size="small" :color="statusColor(r.status)">
{{ statusIcon(r.status) }}
</v-icon>
{{ r.status }}
</td>
<td class="text-right fc-tabular">{{ formatDuration(r) }}</td>
<td class="text-right fc-tabular">{{ formatBytes(r.size_bytes) }}</td>
<td>
<v-chip
v-if="r.tag" size="x-small" color="accent" variant="tonal"
>{{ r.tag }}</v-chip>
<span v-else class="fc-muted"></span>
</td>
<td class="text-right">
<v-menu>
<template #activator="{ props: act }">
<v-btn icon="mdi-dots-vertical" size="x-small" variant="text" v-bind="act" />
</template>
<v-list density="compact">
<v-list-item
:title="r.tag ? 'Untag' : 'Tag…'"
@click="$emit('tag', r)"
/>
<v-list-item
title="Restore…" :disabled="r.status !== 'ok'"
@click="$emit('restore', r)"
/>
<v-list-item
title="Delete…"
@click="$emit('delete', r)"
/>
</v-list>
</v-menu>
</td>
</tr>
<tr v-if="!runs.length">
<td colspan="6" class="text-center fc-muted py-4">No backups yet.</td>
</tr>
</tbody>
</v-table>
</template>
<script setup>
import { formatRelative as fmtRelative } from '../../utils/date.js'
defineProps({ runs: { type: Array, default: () => [] } })
defineEmits(['restore', 'delete', 'tag'])
function statusIcon(s) {
return {
ok: 'mdi-check-circle', error: 'mdi-close-circle',
running: 'mdi-timer-sand', restoring: 'mdi-restore-clock',
restored: 'mdi-restore', pending: 'mdi-clock-outline',
}[s] || 'mdi-help-circle'
}
function statusColor(s) {
return {
ok: 'success', error: 'error',
running: 'accent', restoring: 'info',
restored: 'info', pending: 'on-surface-variant',
}[s] || 'on-surface-variant'
}
function formatDuration(r) {
if (r.duration_seconds == null) return '—'
const s = r.duration_seconds
if (s < 60) return `${s}s`
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60)
return `${h}h ${m}m`
}
function formatBytes(b) {
if (b == null) return '—'
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
let i = 0; let v = b
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
}
function formatRelative(iso) {
return fmtRelative(iso, { nullText: '—' })
}
</script>
<style scoped>
.fc-backup-table { background: transparent; }
.fc-tabular { font-variant-numeric: tabular-nums; }
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
</style>