fc3d: SourceHealthDot — green/amber/red/grey badge with tooltip; embed in SourceRow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<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'
|
||||
|
||||
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}`)
|
||||
|
||||
function relativeTime(iso) {
|
||||
if (!iso) return 'Never'
|
||||
const then = new Date(iso).getTime()
|
||||
const now = Date.now()
|
||||
const diff = Math.abs(now - then) / 1000
|
||||
if (diff < 60) return `${Math.floor(diff)}s`
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
||||
return `${Math.floor(diff / 86400)}d`
|
||||
}
|
||||
|
||||
const lastCheckedText = computed(() => {
|
||||
if (!props.source.last_checked_at) return 'Never'
|
||||
return `${relativeTime(props.source.last_checked_at)} ago`
|
||||
})
|
||||
|
||||
const nextCheckText = computed(() => {
|
||||
if (!props.source.next_check_at) return null
|
||||
const due = new Date(props.source.next_check_at).getTime()
|
||||
if (due <= Date.now()) return 'imminent'
|
||||
return `in ~${relativeTime(props.source.next_check_at)}`
|
||||
})
|
||||
|
||||
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>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="fc-source-row">
|
||||
<SourceHealthDot :source="source" :warning-threshold="warningThreshold" />
|
||||
<v-chip size="x-small" variant="tonal" class="fc-source-row__platform">
|
||||
{{ source.platform }}
|
||||
</v-chip>
|
||||
@@ -22,9 +23,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SourceHealthDot from './SourceHealthDot.vue'
|
||||
|
||||
const props = defineProps({
|
||||
source: { type: Object, required: true },
|
||||
checking: { type: Boolean, default: false },
|
||||
warningThreshold: { type: Number, default: 5 },
|
||||
})
|
||||
const emit = defineEmits(['edit', 'remove', 'toggle', 'check'])
|
||||
function onToggleEnabled(value) {
|
||||
@@ -35,7 +39,7 @@ function onToggleEnabled(value) {
|
||||
<style scoped>
|
||||
.fc-source-row {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr auto auto auto auto;
|
||||
grid-template-columns: auto 96px 1fr auto auto auto auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0;
|
||||
|
||||
Reference in New Issue
Block a user