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.
195 lines
7.6 KiB
TypeScript
195 lines
7.6 KiB
TypeScript
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
||
import { Button } from '@evobgp/ui/components/button'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||
import { Info, RefreshCw } from 'lucide-react'
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||
|
||
import {
|
||
DashboardNetworkCapacityCard,
|
||
NetworkOverviewAnalyticsCard,
|
||
} from '@/components/analytics'
|
||
import { DataGridCard } from '@/components/data-grid-shell'
|
||
import { NetworkPeersGrid } from '@/components/network/network-peers-grid'
|
||
import { NetworkSpeakersGrid } from '@/components/network/network-speakers-grid'
|
||
import { PageHeader } from '@/components/page-header'
|
||
import { QueryState } from '@/components/query-state'
|
||
import { TableSkeleton } from '@/components/skeletons'
|
||
import { networkBirdQueryOptions, networkPeersQueryOptions, networkSpeakersQueryOptions } from '@/queries/network'
|
||
import { overviewJobsQueryOptions } from '@/queries/overview'
|
||
|
||
export const Route = createFileRoute('/_auth/network')({
|
||
component: NetworkComponent,
|
||
validateSearch: (search: Record<string, unknown>) => ({
|
||
tab: (search.tab === 'peers' || search.tab === 'speakers' || search.tab === 'control-plane'
|
||
? search.tab
|
||
: 'overview') as 'overview' | 'peers' | 'speakers' | 'control-plane',
|
||
}),
|
||
})
|
||
|
||
function NetworkComponent() {
|
||
const search = useSearch({ from: '/_auth/network' })
|
||
const peersQ = useQuery({ ...networkPeersQueryOptions(), refetchInterval: 30_000 })
|
||
const speakersQ = useQuery({ ...networkSpeakersQueryOptions(), refetchInterval: 30_000 })
|
||
const birdQ = useQuery({ ...networkBirdQueryOptions(), refetchInterval: 30_000 })
|
||
const jobsQ = useQuery(overviewJobsQueryOptions())
|
||
|
||
const refreshing = peersQ.isFetching || speakersQ.isFetching
|
||
const peers = peersQ.data?.items ?? []
|
||
const speakers = speakersQ.data?.items ?? []
|
||
const jobs = jobsQ.data?.items ?? []
|
||
const overviewLoading = peersQ.isLoading || speakersQ.isLoading
|
||
|
||
function refetchAll() {
|
||
void peersQ.refetch()
|
||
void speakersQ.refetch()
|
||
void birdQ.refetch()
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<PageHeader
|
||
title="Сеть"
|
||
description="BGP-пиры, спикеры и live-метрики нод"
|
||
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>О сетевой конфигурации</AlertTitle>
|
||
<AlertDescription>
|
||
Вкладка «Обзор» — live-статус agent и BGP на CP и репликах. Apply и ревизии — на странице «Операции».
|
||
</AlertDescription>
|
||
</Alert>
|
||
|
||
<Tabs defaultValue={search.tab}>
|
||
<TabsList>
|
||
<TabsTrigger value="overview">Обзор</TabsTrigger>
|
||
<TabsTrigger value="peers">Пиры ({peers.length})</TabsTrigger>
|
||
<TabsTrigger value="speakers">Спикеры ({speakers.length})</TabsTrigger>
|
||
<TabsTrigger value="control-plane">Control plane</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="overview" className="mt-4">
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<NetworkOverviewAnalyticsCard
|
||
peers={peers}
|
||
speakers={speakers}
|
||
loading={overviewLoading}
|
||
/>
|
||
<DashboardNetworkCapacityCard
|
||
peers={peers}
|
||
speakers={speakers}
|
||
jobs={jobs}
|
||
loading={overviewLoading}
|
||
/>
|
||
</div>
|
||
<Card className="mt-4">
|
||
<CardHeader className="border-b py-3">
|
||
<CardTitle className="text-base">BIRD (control plane)</CardTitle>
|
||
<CardDescription>Статус birdc на хосте API</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="p-4">
|
||
<QueryState
|
||
data={birdQ.data}
|
||
isLoading={birdQ.isLoading}
|
||
isError={birdQ.isError}
|
||
error={birdQ.error}
|
||
skeleton={<TableSkeleton rows={3} cols={2} />}
|
||
onRetry={() => birdQ.refetch()}
|
||
>
|
||
{(bird) => <BirdSummary bird={bird} />}
|
||
</QueryState>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="peers" className="mt-4">
|
||
<DataGridCard title="Пиры">
|
||
<QueryState
|
||
data={peers}
|
||
isLoading={peersQ.isLoading}
|
||
isError={peersQ.isError}
|
||
error={peersQ.error}
|
||
empty={peers.length === 0}
|
||
emptyTitle="Нет пиров"
|
||
skeleton={<TableSkeleton rows={6} cols={4} />}
|
||
onRetry={() => peersQ.refetch()}
|
||
>
|
||
{(items) => (
|
||
<NetworkPeersGrid
|
||
items={items}
|
||
isLoading={peersQ.isFetching && !peersQ.isLoading}
|
||
/>
|
||
)}
|
||
</QueryState>
|
||
</DataGridCard>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="speakers" className="mt-4">
|
||
<DataGridCard title="Спикеры">
|
||
<QueryState
|
||
data={speakers}
|
||
isLoading={speakersQ.isLoading}
|
||
isError={speakersQ.isError}
|
||
error={speakersQ.error}
|
||
empty={speakers.length === 0}
|
||
emptyTitle="Нет спикеров"
|
||
skeleton={<TableSkeleton rows={6} cols={4} />}
|
||
onRetry={() => speakersQ.refetch()}
|
||
>
|
||
{(items) => (
|
||
<NetworkSpeakersGrid
|
||
items={items}
|
||
isLoading={speakersQ.isFetching && !speakersQ.isLoading}
|
||
/>
|
||
)}
|
||
</QueryState>
|
||
</DataGridCard>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="control-plane" className="mt-4">
|
||
<Card>
|
||
<CardHeader className="border-b py-3">
|
||
<CardTitle className="text-base">Настройки Control Plane (BIRD)</CardTitle>
|
||
<CardDescription>Конфигурация tenant-level — в разделе «Настройки BIRD»</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="p-4 text-sm text-muted-foreground">
|
||
См. раздел «Настройки BIRD».
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Field({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div className="flex flex-col gap-0.5">
|
||
<span className="text-xs text-muted-foreground">{label}</span>
|
||
<span className="font-medium tabular-nums">{value}</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
|
||
return (
|
||
<div className="flex flex-col gap-2 text-sm">
|
||
<Field
|
||
label="Состояние"
|
||
value={bird.healthy === true ? 'В норме' : bird.healthy === false ? 'Проблема' : 'Н/Д'}
|
||
/>
|
||
<Field label="Сессий BGP" value={`${bird.bgp_established} / ${bird.bgp_sessions_total}`} />
|
||
{bird.message ? <p className="text-xs text-muted-foreground">{bird.message}</p> : null}
|
||
{bird.error ? <p className="text-xs text-destructive">{bird.error}</p> : null}
|
||
</div>
|
||
)
|
||
}
|