CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 49s
CI / go (push) Successful in 1m2s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m57s
Added the AnalyticsDashboardSkeleton component to improve loading states in the dashboard and monitoring pages. Refactored the dashboard to utilize the new skeleton for initial loading, replacing the previous SectionCardsSkeleton. Updated the overview queries to increase job limit from 10 to 100 for better data handling. Enhanced the network and operations components to incorporate new analytics cards, streamlining the user experience and improving data presentation.
216 lines
7.3 KiB
TypeScript
216 lines
7.3 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { useQueries } from '@tanstack/react-query'
|
|
import { CheckCircle, Info, RefreshCw, XCircle } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
|
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
import {
|
|
Card,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@evobgp/ui/components/card'
|
|
import { Skeleton } from '@evobgp/ui/components/skeleton'
|
|
|
|
import {
|
|
DashboardNetworkCapacityCard,
|
|
DashboardOperationsFlowCard,
|
|
DashboardPlatformCard,
|
|
} from '@/components/analytics'
|
|
import { DataGridCard } from '@/components/data-grid-shell'
|
|
import { DashboardQuickActions } from '@/components/dashboard/dashboard-quick-actions'
|
|
import { DashboardRecentJobsGrid } from '@/components/dashboard/dashboard-recent-jobs-grid'
|
|
import { DashboardRecentRevisionsGrid } from '@/components/dashboard/dashboard-recent-revisions-grid'
|
|
import { PageHeader } from '@/components/page-header'
|
|
import { AnalyticsDashboardSkeleton } from '@/components/skeletons'
|
|
|
|
import {
|
|
moduleNameById,
|
|
overviewHealthQueryOptions,
|
|
overviewJobsQueryOptions,
|
|
overviewModulesQueryOptions,
|
|
overviewPeersQueryOptions,
|
|
overviewRevisionsQueryOptions,
|
|
overviewSpeakersQueryOptions,
|
|
} from '@/queries/overview'
|
|
|
|
export const Route = createFileRoute('/_auth/dashboard')({
|
|
component: DashboardComponent,
|
|
})
|
|
|
|
function DashboardComponent() {
|
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
|
|
|
const results = useQueries({
|
|
queries: [
|
|
overviewHealthQueryOptions(),
|
|
overviewModulesQueryOptions(),
|
|
overviewPeersQueryOptions(),
|
|
overviewSpeakersQueryOptions(),
|
|
overviewRevisionsQueryOptions(),
|
|
overviewJobsQueryOptions(),
|
|
],
|
|
})
|
|
|
|
const [healthQ, modulesQ, peersQ, speakersQ, revisionsQ, jobsQ] = results
|
|
const initialLoading =
|
|
modulesQ.isLoading || peersQ.isLoading || speakersQ.isLoading || revisionsQ.isLoading || jobsQ.isLoading
|
|
const refreshing = results.some((r) => r.isFetching && !r.isLoading)
|
|
|
|
if (!lastUpdated && !initialLoading && results.every((r) => r.isSuccess || r.isError)) {
|
|
setTimeout(() => setLastUpdated(new Date()), 0)
|
|
}
|
|
|
|
function refetchAll() {
|
|
setLastUpdated(null)
|
|
results.forEach((r) => r.refetch())
|
|
}
|
|
|
|
const modules = modulesQ.data?.items ?? []
|
|
const peers = peersQ.data?.items ?? []
|
|
const speakers = speakersQ.data?.items ?? []
|
|
const revisions = revisionsQ.data?.items ?? []
|
|
const jobs = jobsQ.data?.items ?? []
|
|
const nameById = moduleNameById(modules)
|
|
|
|
const activityLoading =
|
|
refreshing && jobs.length === 0 && revisions.length === 0
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5">
|
|
<PageHeader
|
|
title="Обзор"
|
|
description={
|
|
lastUpdated
|
|
? `Состояние панели управления EvoBGP. Обновлено: ${lastUpdated.toLocaleTimeString('ru-RU')}`
|
|
: 'Состояние панели управления EvoBGP.'
|
|
}
|
|
actions={
|
|
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
|
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
|
Обновить
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<Alert className="border-info/30 bg-info/5">
|
|
<Info className="text-info" />
|
|
<AlertTitle>Панель управления EvoBGP</AlertTitle>
|
|
<AlertDescription>
|
|
Сводка по модулям, сети и фоновым задачам. BGP и ноды — «Сеть», префиксы — «Модули»,
|
|
деплой — «Операции», здоровье API — «Мониторинг».
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
<HealthAlert
|
|
loading={healthQ.isLoading}
|
|
ok={healthQ.data === true}
|
|
loadError={modulesQ.isError || peersQ.isError ? 'Некоторые данные не загружены' : null}
|
|
/>
|
|
|
|
{initialLoading ? (
|
|
<AnalyticsDashboardSkeleton />
|
|
) : (
|
|
<div className="grid gap-4 lg:grid-cols-3 lg:grid-rows-2">
|
|
<div className="lg:row-span-2">
|
|
<DashboardPlatformCard
|
|
modules={modules}
|
|
peers={peers}
|
|
speakers={speakers}
|
|
jobs={jobs}
|
|
revisions={revisions}
|
|
/>
|
|
</div>
|
|
<DashboardNetworkCapacityCard peers={peers} speakers={speakers} jobs={jobs} />
|
|
<DashboardOperationsFlowCard jobs={jobs} modules={modules} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<DataGridCard
|
|
title="Недавние задачи"
|
|
description="Последние фоновые операции"
|
|
className="h-full"
|
|
>
|
|
{activityLoading ? (
|
|
<Skeleton className="m-3 h-24 w-auto" />
|
|
) : (
|
|
<DashboardRecentJobsGrid jobs={jobs.slice(0, 10)} nameById={nameById} isLoading={refreshing} />
|
|
)}
|
|
</DataGridCard>
|
|
|
|
<DataGridCard
|
|
title="Последние ревизии"
|
|
description="История конфигураций"
|
|
className="h-full"
|
|
>
|
|
{activityLoading ? (
|
|
<Skeleton className="m-3 h-24 w-auto" />
|
|
) : (
|
|
<DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} />
|
|
)}
|
|
</DataGridCard>
|
|
</div>
|
|
|
|
<Card className="gap-0">
|
|
<CardHeader className="border-b py-3">
|
|
<CardTitle className="text-base">Быстрые действия</CardTitle>
|
|
<CardDescription>Частые переходы к настройке и деплою</CardDescription>
|
|
</CardHeader>
|
|
<DashboardQuickActions />
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function HealthAlert({
|
|
loading,
|
|
ok,
|
|
loadError,
|
|
}: {
|
|
loading: boolean
|
|
ok: boolean | undefined
|
|
loadError: string | null
|
|
}) {
|
|
if (loading) {
|
|
return (
|
|
<Alert>
|
|
<Skeleton className="size-5 rounded-full" />
|
|
<AlertTitle>Проверка API…</AlertTitle>
|
|
<AlertDescription>
|
|
Запрос к <code className="text-xs">/v1/health</code>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
if (ok && !loadError) {
|
|
return (
|
|
<Alert className="border-success/30 bg-success/5">
|
|
<CheckCircle className="text-success" />
|
|
<AlertTitle>API работает</AlertTitle>
|
|
<AlertDescription>Сервер отвечает на запросы health-check.</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
if (ok && loadError) {
|
|
return (
|
|
<Alert className="border-warning/30 bg-warning/5">
|
|
<Info className="text-warning" />
|
|
<AlertTitle>API доступен, данные не загружены</AlertTitle>
|
|
<AlertDescription>{loadError}. Проверьте Bearer-токен в «Настройках».</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
return (
|
|
<Alert variant="destructive" className="border-destructive/30 bg-destructive/5">
|
|
<XCircle className="text-destructive" />
|
|
<AlertTitle>API недоступен</AlertTitle>
|
|
<AlertDescription>
|
|
Не удалось получить ответ от сервера. Проверьте, что API запущен (порт 8080) и в dev работает
|
|
прокси Vite.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|