quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 22s
quality / web (push) Successful in 1m0s
CD / quality (push) Successful in 1m35s
CD / publish (push) Successful in 2m59s
Секции страниц переведены на segmented Tabs (c-tabs-9), статус грида — ToggleGroup в toolbar. Убраны календарь задач, пустые вкладки мониторинга и demo-деревья ReUI. KPI overflow как в CFDM, DetailPanel на карточке модуля, один FrameSection. Co-authored-by: Cursor <[email protected]>
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import { useNavigate } from '@tanstack/react-router'
|
|
import { useMemo } from 'react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
|
|
import { AnalyticsActivityList } from '@/components/analytics/analytics-activity-list'
|
|
import { AnalyticsCardShell } from '@/components/analytics/analytics-card-shell'
|
|
import { AnalyticsKpiRow } from '@/components/analytics/analytics-kpi-row'
|
|
import { AnalyticsProgress } from '@/components/analytics/analytics-progress'
|
|
import {
|
|
deploymentProgress,
|
|
deploymentProgressMeta,
|
|
recentPlatformActivity,
|
|
} from '@/lib/metrics'
|
|
import { runningJobCount } from '@/queries/overview'
|
|
import type { JobRow, ModuleRow, PeerRow, RevisionRow, SpeakerRow } from '@/types/api'
|
|
|
|
export function DashboardPlatformCard({
|
|
modules,
|
|
peers,
|
|
speakers,
|
|
jobs,
|
|
revisions,
|
|
loading,
|
|
}: {
|
|
modules: ModuleRow[]
|
|
peers: PeerRow[]
|
|
speakers: SpeakerRow[]
|
|
jobs: JobRow[]
|
|
revisions: RevisionRow[]
|
|
loading?: boolean
|
|
}) {
|
|
const navigate = useNavigate()
|
|
|
|
const enabledModules = modules.filter((m) => m.enabled !== false).length
|
|
const peersEnabled = peers.filter((p) => p.enabled !== false).length
|
|
const peersEstablished = peers.filter(
|
|
(p) => p.enabled !== false && p.session_state === 'Established',
|
|
).length
|
|
const peersMismatch = peers.filter((p) => p.session_mismatch).length
|
|
const speakersOnline = speakers.filter((s) => s.live?.agent_ok).length
|
|
const failedJobs = jobs.filter((j) =>
|
|
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
|
).length
|
|
const running = runningJobCount(jobs)
|
|
const riskCount = peersMismatch + failedJobs + Math.max(0, speakers.length - speakersOnline)
|
|
|
|
const bgpPct =
|
|
peersEnabled > 0 ? Math.round((peersEstablished / peersEnabled) * 100) : null
|
|
|
|
const deploy = useMemo(() => deploymentProgress(speakers), [speakers])
|
|
const deployMeta = useMemo(() => deploymentProgressMeta(deploy), [deploy])
|
|
const activity = useMemo(
|
|
() => recentPlatformActivity(jobs, revisions, peers, speakers),
|
|
[jobs, revisions, peers, speakers],
|
|
)
|
|
|
|
const kpis = [
|
|
{
|
|
label: 'Модули активны',
|
|
value: loading ? '—' : `${enabledModules}/${modules.length || 0}`,
|
|
delta: {
|
|
direction: 'neutral' as const,
|
|
label: `${modules.length} всего`,
|
|
tone: 'muted' as const,
|
|
},
|
|
},
|
|
{
|
|
label: 'BGP готовность',
|
|
value: loading || bgpPct === null ? '—' : `${bgpPct}%`,
|
|
delta: {
|
|
direction: (bgpPct !== null && bgpPct >= 90 ? 'up' : bgpPct !== null && bgpPct < 70 ? 'down' : 'neutral') as
|
|
| 'up'
|
|
| 'down'
|
|
| 'neutral',
|
|
label:
|
|
bgpPct === null
|
|
? 'нет включённых пиров'
|
|
: `${peersEstablished} установлено`,
|
|
tone: (bgpPct !== null && bgpPct >= 90
|
|
? 'success'
|
|
: bgpPct !== null && bgpPct < 70
|
|
? 'warning'
|
|
: 'muted') as 'success' | 'warning' | 'muted',
|
|
},
|
|
},
|
|
{
|
|
label: 'Риски',
|
|
value: loading ? '—' : String(riskCount),
|
|
delta: {
|
|
direction: (riskCount > 0 ? 'down' : 'up') as 'up' | 'down',
|
|
label: riskCount > 0 ? `${failedJobs} задач, ${peersMismatch} расхождений` : 'в норме',
|
|
tone: (riskCount > 0 ? 'destructive' : 'success') as 'destructive' | 'success',
|
|
},
|
|
},
|
|
]
|
|
|
|
const progressLabel = deployMeta.label
|
|
const progressHint = deployMeta.hint
|
|
|
|
return (
|
|
<AnalyticsCardShell
|
|
title="Состояние платформы"
|
|
description="Сводка модулей, BGP и фоновых задач"
|
|
info="Актуальный снимок без исторических трендов. Обновите данные кнопкой «Обновить» на странице."
|
|
footer={
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
className="flex-1"
|
|
onClick={() => navigate({ to: '/schedule' })}
|
|
>
|
|
Расписание
|
|
</Button>
|
|
<Button
|
|
className="flex-1"
|
|
onClick={() => navigate({ to: '/monitoring' })}
|
|
>
|
|
Мониторинг
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<AnalyticsKpiRow items={kpis} />
|
|
<AnalyticsProgress
|
|
label={progressLabel}
|
|
hint={progressHint}
|
|
value={loading ? 0 : deploy.percent}
|
|
/>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Недавняя активность</span>
|
|
{!loading ? (
|
|
<span className="text-xs text-muted-foreground">{running} активных задач</span>
|
|
) : null}
|
|
</div>
|
|
{loading ? (
|
|
<p className="text-sm text-muted-foreground">Загрузка…</p>
|
|
) : (
|
|
<AnalyticsActivityList items={activity} />
|
|
)}
|
|
</div>
|
|
</AnalyticsCardShell>
|
|
)
|
|
}
|