quality / changes (push) Successful in 9s
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / web (push) Successful in 1m29s
quality / api (push) Successful in 1m10s
CD / quality (push) Successful in 2m54s
CD / publish (push) Successful in 42s
Собрать панель, списки и настройки в одной оболочке, чтобы статусы, очередь и свежесть данных были видны на доменах, сервисах и группах. Co-authored-by: Cursor <[email protected]>
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
|
|
import { Switch } from '@cfdm/ui/components/switch'
|
|
import { formatRelative } from '@/lib/format'
|
|
import { isLiveRefresh, setLiveRefresh } from '@/lib/live-refresh'
|
|
|
|
function useLatestDataUpdatedAt() {
|
|
const queryClient = useQueryClient()
|
|
const [updatedAt, setUpdatedAt] = useState(0)
|
|
|
|
useEffect(() => {
|
|
const cache = queryClient.getQueryCache()
|
|
let queued = false
|
|
let active = true
|
|
const read = () => {
|
|
let latest = 0
|
|
for (const query of cache.getAll()) {
|
|
if (query.getObserversCount() === 0) continue
|
|
if (query.state.dataUpdatedAt > latest) latest = query.state.dataUpdatedAt
|
|
}
|
|
setUpdatedAt((prev) => (prev === latest ? prev : latest))
|
|
}
|
|
// Cache notifies synchronously while another route renders (observerAdded).
|
|
const schedule = () => {
|
|
if (queued) return
|
|
queued = true
|
|
queueMicrotask(() => {
|
|
queued = false
|
|
if (active) read()
|
|
})
|
|
}
|
|
read()
|
|
const unsubscribe = cache.subscribe(schedule)
|
|
return () => {
|
|
active = false
|
|
unsubscribe()
|
|
}
|
|
}, [queryClient])
|
|
|
|
return updatedAt
|
|
}
|
|
|
|
function formatUpdatedAt(updatedAt: number) {
|
|
if (updatedAt === 0) return '—'
|
|
return formatRelative(new Date(updatedAt).toISOString())
|
|
}
|
|
|
|
/** Компактный статус свежести слева от AppsMenu. */
|
|
export function DataFreshness() {
|
|
const queryClient = useQueryClient()
|
|
const updatedAt = useLatestDataUpdatedAt()
|
|
const [live, setLive] = useState(() => isLiveRefresh())
|
|
const [label, setLabel] = useState('—')
|
|
|
|
useEffect(() => {
|
|
const read = () => setLabel(formatUpdatedAt(updatedAt))
|
|
read()
|
|
if (updatedAt === 0) return
|
|
const timer = window.setInterval(read, 15_000)
|
|
return () => window.clearInterval(timer)
|
|
}, [updatedAt])
|
|
|
|
function handleCheckedChange(checked: boolean) {
|
|
const next = Boolean(checked)
|
|
setLiveRefresh(queryClient, next)
|
|
setLive(next)
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-muted-foreground hidden items-center gap-1.5 text-xs sm:flex">
|
|
<span className="tabular-nums">
|
|
Обновлено{' '}
|
|
<time dateTime={updatedAt ? new Date(updatedAt).toISOString() : undefined}>
|
|
{label}
|
|
</time>
|
|
</span>
|
|
<span aria-hidden="true">·</span>
|
|
<span>{live ? 'Live' : 'Пауза'}</span>
|
|
</p>
|
|
<Switch
|
|
size="sm"
|
|
checked={live}
|
|
onCheckedChange={handleCheckedChange}
|
|
aria-label={live ? 'Выключить автообновление' : 'Включить автообновление'}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|