Раскрытие строки задачи показывает вертикальный ReUI Timeline: создана (с триггером) -> взята в работу -> результат с текстом ошибки. Diff ревизий переведён с самодельных <pre> на официальный @reui/code-block: единый блок с разметкой добавленных и удалённых строк (знаки +/- и тонировка), нумерацией, копированием и разворотом длинных списков. ResourcePage (filtered-вариант) получил поддержку expandedContent/getRowCanExpand — колонка раскрытия строится так же, как во FrameDataGrid. Точечные упрощения: прогресс BGP-сессий в мониторинге переведён на Progress из кита вместо самодельного бара; удалены @deprecated-константы ui-surface.ts, мёртвый data-grid-defaults.ts и неиспользуемый sparkline-ряд дашборда.
184 lines
6.2 KiB
TypeScript
184 lines
6.2 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Bird, RefreshCw } from 'lucide-react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
import { Progress } from '@evobgp/ui/components/progress'
|
|
import { MonitoringHealthCard } from '@/components/analytics'
|
|
import { MonitoringReadyGrid } from '@/components/monitoring/monitoring-ready-grid'
|
|
import { PageHeader } from '@/components/page-header'
|
|
import { QueryState } from '@/components/query-state'
|
|
import { FrameSection } from '@/components/reui-kit'
|
|
import { StatusBadge } from '@/components/status-badge'
|
|
import { jobKindRu } from '@/lib/ui-labels'
|
|
|
|
import {
|
|
monitoringHealthQueryOptions,
|
|
monitoringReadyQueryOptions,
|
|
monitoringVersionQueryOptions,
|
|
type VersionInfo,
|
|
} from '@/queries/monitoring'
|
|
import { networkBirdQueryOptions } from '@/queries/network'
|
|
import { operationsJobsQueryOptions } from '@/queries/operations'
|
|
|
|
export const Route = createFileRoute('/_auth/monitoring')({
|
|
component: MonitoringComponent,
|
|
})
|
|
|
|
function MonitoringComponent() {
|
|
const healthQ = useQuery(monitoringHealthQueryOptions())
|
|
const readyQ = useQuery(monitoringReadyQueryOptions())
|
|
const versionQ = useQuery(monitoringVersionQueryOptions())
|
|
const birdQ = useQuery(networkBirdQueryOptions())
|
|
const jobsQ = useQuery(operationsJobsQueryOptions())
|
|
|
|
const refreshing =
|
|
healthQ.isFetching ||
|
|
readyQ.isFetching ||
|
|
versionQ.isFetching ||
|
|
birdQ.isFetching ||
|
|
jobsQ.isFetching
|
|
|
|
const jobs = jobsQ.data?.items ?? []
|
|
const versionText = formatVersion(versionQ.data)
|
|
|
|
function refetchAll() {
|
|
void healthQ.refetch()
|
|
void readyQ.refetch()
|
|
void versionQ.refetch()
|
|
void birdQ.refetch()
|
|
void jobsQ.refetch()
|
|
}
|
|
|
|
const failedJobs = jobs
|
|
.filter((j) => ['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()))
|
|
.slice(0, 5)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 md:gap-6">
|
|
<PageHeader
|
|
title="Мониторинг"
|
|
description={`Состояние API, BGP и задач для диагностики инцидентов${
|
|
versionText !== '—'
|
|
? ` · версия ${versionText}${
|
|
versionQ.data?.git_sha ? ` (${versionQ.data.git_sha.slice(0, 8)})` : ''
|
|
}`
|
|
: ''
|
|
}`}
|
|
actions={
|
|
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
|
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
|
Обновить
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<div className="grid items-stretch gap-4 md:gap-6 lg:grid-cols-2">
|
|
<MonitoringHealthCard
|
|
healthOk={healthQ.data?.ok ?? false}
|
|
ready={readyQ.data}
|
|
loading={healthQ.isLoading || readyQ.isLoading}
|
|
/>
|
|
<FrameSection
|
|
title={
|
|
<span className="flex items-center gap-2">
|
|
<Bird className="size-4" />
|
|
BGP на API-хосте
|
|
</span>
|
|
}
|
|
description="Статус BIRD на хосте API"
|
|
className="h-full"
|
|
contentClassName="px-5 py-4"
|
|
>
|
|
<QueryState
|
|
data={birdQ.data}
|
|
isLoading={birdQ.isLoading}
|
|
isError={birdQ.isError}
|
|
error={birdQ.error}
|
|
skeleton={<div className="h-40" />}
|
|
onRetry={() => birdQ.refetch()}
|
|
>
|
|
{(bird) => <BirdSummary bird={bird} />}
|
|
</QueryState>
|
|
</FrameSection>
|
|
</div>
|
|
|
|
<QueryState
|
|
data={readyQ.data}
|
|
isLoading={readyQ.isLoading}
|
|
isError={readyQ.isError}
|
|
error={readyQ.error}
|
|
skeleton={<div className="h-40" />}
|
|
onRetry={() => readyQ.refetch()}
|
|
>
|
|
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
|
</QueryState>
|
|
|
|
{failedJobs.length > 0 ? (
|
|
<FrameSection title="Последние ошибки" contentClassName="px-5 py-4">
|
|
<ul className="flex flex-col gap-2">
|
|
{failedJobs.map((job) => (
|
|
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="font-medium">{jobKindRu(job.kind)}</p>
|
|
<StatusBadge status={job.status} />
|
|
</div>
|
|
{job.error ? (
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{job.error.slice(0, 120)}
|
|
{job.error.length > 120 ? '…' : ''}
|
|
</p>
|
|
) : null}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</FrameSection>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatVersion(version?: VersionInfo | null): string {
|
|
if (!version) return '—'
|
|
return version.version ?? version.app ?? '—'
|
|
}
|
|
|
|
function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
|
|
if (!bird.birdc_configured) {
|
|
return (
|
|
<p className="text-sm text-muted-foreground">
|
|
{bird.message ?? 'birdc не настроен на API-хосте (EVOBGP_BIRDC_SOCKET).'}
|
|
</p>
|
|
)
|
|
}
|
|
const ratio =
|
|
bird.bgp_sessions_total > 0
|
|
? Math.round((bird.bgp_established / bird.bgp_sessions_total) * 100)
|
|
: null
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Установлено / всего</span>
|
|
<span className="font-medium tabular-nums">
|
|
{bird.bgp_established} / {bird.bgp_sessions_total}
|
|
{ratio !== null ? <span className="text-muted-foreground"> ({ratio}%)</span> : null}
|
|
</span>
|
|
</div>
|
|
{ratio !== null ? (
|
|
<Progress
|
|
value={ratio}
|
|
aria-label="Установлено BGP-сессий"
|
|
className={
|
|
ratio >= 100
|
|
? '[&_[data-slot=progress-indicator]]:bg-success'
|
|
: ratio >= 50
|
|
? '[&_[data-slot=progress-indicator]]:bg-warning'
|
|
: '[&_[data-slot=progress-indicator]]:bg-destructive'
|
|
}
|
|
/>
|
|
) : null}
|
|
{bird.error ? <p className="text-xs text-destructive">{bird.error}</p> : null}
|
|
</div>
|
|
)
|
|
}
|