- 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>
80 lines
2.3 KiB
Vue
80 lines
2.3 KiB
Vue
<template>
|
|
<v-tooltip location="top" open-delay="200">
|
|
<template #activator="{ props: tipProps }">
|
|
<span
|
|
v-bind="tipProps"
|
|
:class="['fc-health-dot', `fc-health-dot--${level}`]"
|
|
:aria-label="ariaLabel"
|
|
/>
|
|
</template>
|
|
<div class="fc-health-tip">
|
|
<div>Last checked: {{ lastCheckedText }}</div>
|
|
<div v-if="nextCheckText">Next check: {{ nextCheckText }}</div>
|
|
<div v-if="(source.consecutive_failures || 0) > 0">
|
|
Failures: {{ source.consecutive_failures }}
|
|
</div>
|
|
<div v-if="source.last_error" class="fc-health-tip__err">
|
|
{{ truncatedError }}
|
|
</div>
|
|
</div>
|
|
</v-tooltip>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import { formatRelative } from '../../utils/date.js'
|
|
|
|
const props = defineProps({
|
|
source: { type: Object, required: true },
|
|
warningThreshold: { type: Number, default: 5 },
|
|
})
|
|
|
|
const level = computed(() => {
|
|
if (!props.source.last_checked_at) return 'unchecked'
|
|
const f = props.source.consecutive_failures || 0
|
|
if (f === 0) return 'healthy'
|
|
if (f < props.warningThreshold) return 'warning'
|
|
return 'critical'
|
|
})
|
|
|
|
const ariaLabel = computed(() => `source health: ${level.value}`)
|
|
|
|
const lastCheckedText = computed(() => formatRelative(props.source.last_checked_at))
|
|
|
|
const nextCheckText = computed(() =>
|
|
props.source.next_check_at
|
|
? formatRelative(props.source.next_check_at, { future: true })
|
|
: null,
|
|
)
|
|
|
|
const truncatedError = computed(() => {
|
|
const e = props.source.last_error || ''
|
|
return e.length > 120 ? e.slice(0, 117) + '…' : e
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-health-dot {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.fc-health-dot--unchecked { background-color: rgb(var(--v-theme-on-surface-variant)); opacity: 0.5; }
|
|
.fc-health-dot--healthy { background-color: rgb(var(--v-theme-success, 76 175 80)); }
|
|
.fc-health-dot--warning { background-color: rgb(var(--v-theme-warning, 255 167 38)); }
|
|
.fc-health-dot--critical { background-color: rgb(var(--v-theme-error, 244 67 54)); }
|
|
|
|
.fc-health-tip {
|
|
font-size: 0.85rem;
|
|
line-height: 1.4;
|
|
}
|
|
.fc-health-tip__err {
|
|
margin-top: 0.25rem;
|
|
color: rgb(var(--v-theme-error, 244 67 54));
|
|
white-space: pre-wrap;
|
|
max-width: 32rem;
|
|
}
|
|
</style>
|