Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac727ad1e3 |
@@ -34,7 +34,7 @@ export function AnalyticsCardShell({
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className={cn('flex h-full flex-col gap-0 overflow-hidden', className)}>
|
<Card className={cn('flex flex-col gap-0 overflow-hidden', className)}>
|
||||||
<CardHeader className="flex flex-row items-start justify-between gap-3 border-b py-4">
|
<CardHeader className="flex flex-row items-start justify-between gap-3 border-b py-4">
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
@@ -57,7 +57,7 @@ export function AnalyticsCardShell({
|
|||||||
</div>
|
</div>
|
||||||
{actions ? <div className="shrink-0">{actions}</div> : null}
|
{actions ? <div className="shrink-0">{actions}</div> : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-1 flex-col gap-5 p-5">{children}</CardContent>
|
<CardContent className="flex flex-col gap-5 p-5">{children}</CardContent>
|
||||||
{footer ? <CardFooter className="gap-2 border-t p-4">{footer}</CardFooter> : null}
|
{footer ? <CardFooter className="gap-2 border-t p-4">{footer}</CardFooter> : null}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import {
|
|||||||
DrawerClose,
|
DrawerClose,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from '@evobgp/ui/components/drawer'
|
} from '@evobgp/ui/components/drawer'
|
||||||
import type { ReactElement, ReactNode } from 'react'
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
confirmDrawerContentClassName,
|
||||||
|
DrawerActionsFooter,
|
||||||
|
} from '@/components/drawer-layout'
|
||||||
|
|
||||||
type ConfirmDialogBaseProps = {
|
type ConfirmDialogBaseProps = {
|
||||||
title: string
|
title: string
|
||||||
description?: ReactNode
|
description?: ReactNode
|
||||||
@@ -58,11 +62,11 @@ function ConfirmDrawerBody({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader>
|
<DrawerHeader className="shrink-0 border-b border-border pb-4">
|
||||||
<DrawerTitle>{title}</DrawerTitle>
|
<DrawerTitle>{title}</DrawerTitle>
|
||||||
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerFooter className="border-t bg-muted/50 sm:flex-row sm:justify-end">
|
<DrawerActionsFooter>
|
||||||
<DrawerClose render={<Button variant="outline" disabled={confirmLoading} />}>
|
<DrawerClose render={<Button variant="outline" disabled={confirmLoading} />}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
@@ -87,7 +91,7 @@ function ConfirmDrawerBody({
|
|||||||
{confirmText}
|
{confirmText}
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
)}
|
)}
|
||||||
</DrawerFooter>
|
</DrawerActionsFooter>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -109,7 +113,7 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
|||||||
return (
|
return (
|
||||||
<Drawer swipeDirection="right">
|
<Drawer swipeDirection="right">
|
||||||
<DrawerTrigger render={props.trigger} />
|
<DrawerTrigger render={props.trigger} />
|
||||||
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
<DrawerContent className={confirmDrawerContentClassName}>
|
||||||
<ConfirmDrawerBody {...bodyProps} />
|
<ConfirmDrawerBody {...bodyProps} />
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@@ -118,7 +122,7 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={props.open} onOpenChange={props.onOpenChange} swipeDirection="right">
|
<Drawer open={props.open} onOpenChange={props.onOpenChange} swipeDirection="right">
|
||||||
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
<DrawerContent className={confirmDrawerContentClassName}>
|
||||||
<ConfirmDrawerBody {...bodyProps} controlled />
|
<ConfirmDrawerBody {...bodyProps} controlled />
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@evobgp/ui/lib/utils'
|
||||||
|
import { DrawerFooter } from '@evobgp/ui/components/drawer'
|
||||||
|
|
||||||
|
/** Shared footer layout for right-side form and confirm drawers. */
|
||||||
|
export function DrawerActionsFooter({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DrawerFooter
|
||||||
|
className={cn(
|
||||||
|
'mt-0 shrink-0 border-t border-border bg-muted/50 p-4',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</DrawerFooter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formDrawerContentClassName =
|
||||||
|
'flex h-full max-h-dvh flex-col sm:max-w-lg'
|
||||||
|
|
||||||
|
export const confirmDrawerContentClassName = 'flex h-auto max-h-dvh flex-col sm:max-w-sm'
|
||||||
@@ -5,12 +5,16 @@ import {
|
|||||||
Drawer,
|
Drawer,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
} from '@evobgp/ui/components/drawer'
|
} from '@evobgp/ui/components/drawer'
|
||||||
import { ScrollArea } from '@evobgp/ui/components/scroll-area'
|
import { ScrollArea } from '@evobgp/ui/components/scroll-area'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DrawerActionsFooter,
|
||||||
|
formDrawerContentClassName,
|
||||||
|
} from '@/components/drawer-layout'
|
||||||
|
|
||||||
interface FormDrawerProps {
|
interface FormDrawerProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
@@ -32,15 +36,15 @@ export function FormDrawer({
|
|||||||
}: FormDrawerProps) {
|
}: FormDrawerProps) {
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={onOpenChange} swipeDirection="right">
|
<Drawer open={open} onOpenChange={onOpenChange} swipeDirection="right">
|
||||||
<DrawerContent className={cn('h-full max-h-none sm:max-w-lg', className)}>
|
<DrawerContent className={cn(formDrawerContentClassName, className)}>
|
||||||
<DrawerHeader>
|
<DrawerHeader className="shrink-0 border-b border-border pb-4">
|
||||||
<DrawerTitle>{title}</DrawerTitle>
|
<DrawerTitle>{title}</DrawerTitle>
|
||||||
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<ScrollArea className="min-h-0 flex-1 px-4">
|
<ScrollArea className="min-h-0 flex-1">
|
||||||
<div className="space-y-4 pb-4">{children}</div>
|
<div className="space-y-4 px-4 py-4">{children}</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<DrawerFooter className="border-t bg-muted/50 sm:flex-row sm:justify-end">{footer}</DrawerFooter>
|
<DrawerActionsFooter>{footer}</DrawerActionsFooter>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export function SectionCardsSkeleton({ count = 4 }: { count?: number }) {
|
|||||||
|
|
||||||
export function AnalyticsDashboardSkeleton() {
|
export function AnalyticsDashboardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 lg:grid-cols-3 lg:grid-rows-2">
|
<div className="grid gap-4 lg:grid-cols-3 lg:items-start">
|
||||||
<Card className="gap-0 lg:row-span-2">
|
<Card className="gap-0">
|
||||||
<CardContent className="space-y-4 p-5">
|
<CardContent className="space-y-4 p-5">
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40" />
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Info, KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react'
|
import { KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
|
|
||||||
@@ -79,22 +78,6 @@ function AccessComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>О API-ключах</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Роли: <code className="text-xs">viewer</code> (чтение),{' '}
|
|
||||||
<code className="text-xs">editor</code> (CRUD), <code className="text-xs">operator</code>{' '}
|
|
||||||
(apply и настройки), <code className="text-xs">node</code> (API ноды). Полный токен
|
|
||||||
показывается один раз при создании и ротации. Токен браузера — в{' '}
|
|
||||||
<Link to="/settings" className="text-primary underline-offset-4 hover:underline">
|
|
||||||
настройках
|
|
||||||
</Link>
|
|
||||||
; для локальной разработки с demo-seed подойдёт <code className="text-xs">dev</code>{' '}
|
|
||||||
(роль operator).
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{session ? (
|
{session ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="border-b py-3">
|
<CardHeader className="border-b py-3">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQueries } from '@tanstack/react-query'
|
import { useQueries } from '@tanstack/react-query'
|
||||||
import { CheckCircle, Info, RefreshCw, XCircle } from 'lucide-react'
|
import { RefreshCw } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -27,7 +26,6 @@ import { AnalyticsDashboardSkeleton } from '@/components/skeletons'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
moduleNameById,
|
moduleNameById,
|
||||||
overviewHealthQueryOptions,
|
|
||||||
overviewJobsQueryOptions,
|
overviewJobsQueryOptions,
|
||||||
overviewModulesQueryOptions,
|
overviewModulesQueryOptions,
|
||||||
overviewPeersQueryOptions,
|
overviewPeersQueryOptions,
|
||||||
@@ -44,7 +42,6 @@ function DashboardComponent() {
|
|||||||
|
|
||||||
const results = useQueries({
|
const results = useQueries({
|
||||||
queries: [
|
queries: [
|
||||||
overviewHealthQueryOptions(),
|
|
||||||
overviewModulesQueryOptions(),
|
overviewModulesQueryOptions(),
|
||||||
overviewPeersQueryOptions(),
|
overviewPeersQueryOptions(),
|
||||||
overviewSpeakersQueryOptions(),
|
overviewSpeakersQueryOptions(),
|
||||||
@@ -53,7 +50,7 @@ function DashboardComponent() {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const [healthQ, modulesQ, peersQ, speakersQ, revisionsQ, jobsQ] = results
|
const [modulesQ, peersQ, speakersQ, revisionsQ, jobsQ] = results
|
||||||
const initialLoading =
|
const initialLoading =
|
||||||
modulesQ.isLoading || peersQ.isLoading || speakersQ.isLoading || revisionsQ.isLoading || jobsQ.isLoading
|
modulesQ.isLoading || peersQ.isLoading || speakersQ.isLoading || revisionsQ.isLoading || jobsQ.isLoading
|
||||||
const refreshing = results.some((r) => r.isFetching && !r.isLoading)
|
const refreshing = results.some((r) => r.isFetching && !r.isLoading)
|
||||||
@@ -94,34 +91,17 @@ function DashboardComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 ? (
|
{initialLoading ? (
|
||||||
<AnalyticsDashboardSkeleton />
|
<AnalyticsDashboardSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 lg:grid-cols-3 lg:grid-rows-2">
|
<div className="grid gap-4 lg:grid-cols-3 lg:items-start">
|
||||||
<div className="lg:row-span-2">
|
<DashboardPlatformCard
|
||||||
<DashboardPlatformCard
|
modules={modules}
|
||||||
modules={modules}
|
peers={peers}
|
||||||
peers={peers}
|
speakers={speakers}
|
||||||
speakers={speakers}
|
jobs={jobs}
|
||||||
jobs={jobs}
|
revisions={revisions}
|
||||||
revisions={revisions}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DashboardNetworkCapacityCard peers={peers} speakers={speakers} jobs={jobs} />
|
<DashboardNetworkCapacityCard peers={peers} speakers={speakers} jobs={jobs} />
|
||||||
<DashboardOperationsFlowCard jobs={jobs} modules={modules} />
|
<DashboardOperationsFlowCard jobs={jobs} modules={modules} />
|
||||||
</div>
|
</div>
|
||||||
@@ -163,53 +143,3 @@ function DashboardComponent() {
|
|||||||
</div>
|
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { BookText, Globe, Info, RefreshCw, Tags } from 'lucide-react'
|
import { BookText, Globe, RefreshCw, Tags } from 'lucide-react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
import { DataGridCard } from '@/components/data-grid-shell'
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
@@ -68,15 +67,6 @@ function DirectoriesComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>О справочниках</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Сообщества BGP используются в AS- и CDN-модулях для тегирования префиксов. DoH-профили — в
|
|
||||||
доменных модулях для DNS-over-HTTPS резолвинга.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
||||||
|
|
||||||
<BadgeTabs
|
<BadgeTabs
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Copy, Info, Plus, RefreshCw, Shield } from 'lucide-react'
|
import { Copy, Plus, RefreshCw, Shield } from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
@@ -126,16 +125,6 @@ function FirewallPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Политика</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Правила сопоставляются с <strong>BGP community</strong> префиксов опубликованной revision.{' '}
|
|
||||||
<strong>block</strong> добавляет префиксы community в kernel; <strong>accept</strong> — не блокирует.
|
|
||||||
Community «Все» — правило для любого community. Default без совпадений — accept.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Info, RefreshCw } from 'lucide-react'
|
import { ArrowLeft, RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
|
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
@@ -17,25 +16,12 @@ import {
|
|||||||
directoriesDohQueryOptions,
|
directoriesDohQueryOptions,
|
||||||
} from '@/queries/directories'
|
} from '@/queries/directories'
|
||||||
import { moduleDetailQueryOptions, moduleEntriesQueryOptions, modulesKeys } from '@/queries/modules'
|
import { moduleDetailQueryOptions, moduleEntriesQueryOptions, modulesKeys } from '@/queries/modules'
|
||||||
import type { AsEntry, ModuleRow } from '@/types/api'
|
import type { AsEntry } from '@/types/api'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/modules/$moduleId')({
|
export const Route = createFileRoute('/_auth/modules/$moduleId')({
|
||||||
component: ModuleDetailComponent,
|
component: ModuleDetailComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
function moduleTypeAlert(type: ModuleRow['type']): string {
|
|
||||||
switch (type) {
|
|
||||||
case 'AS_PREFIXES':
|
|
||||||
return 'Модуль AS получает префиксы через RIPEstat по указанным ASN. После refresh счётчики префиксов обновляются в таблице записей.'
|
|
||||||
case 'CDN_CIDRS':
|
|
||||||
return 'Модуль CDN скачивает списки CIDR по URL (plaintext или JSON). Используйте предпросмотр при добавлении источника.'
|
|
||||||
case 'DOMAINS':
|
|
||||||
return 'Модуль доменов резолвит FQDN через DoH-профили и конвертирует IP в префиксы. Политика и профили настраиваются в редактировании модуля.'
|
|
||||||
case 'IP_RANGES':
|
|
||||||
return 'Модуль IP-диапазонов использует статические CIDR без внешнего refresh (сервер может вернуть 204). Записи участвуют в агрегации напрямую.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModuleDetailComponent() {
|
function ModuleDetailComponent() {
|
||||||
const { moduleId } = Route.useParams()
|
const { moduleId } = Route.useParams()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -114,12 +100,6 @@ function ModuleDetailComponent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<Info />
|
|
||||||
<AlertTitle>О модуле</AlertTitle>
|
|
||||||
<AlertDescription>{moduleTypeAlert(m.type)}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<ModuleKpiCards
|
<ModuleKpiCards
|
||||||
mod={m}
|
mod={m}
|
||||||
communities={communities}
|
communities={communities}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Activity, AlertTriangle, Bird, Database, HeartPulse, Info, ListTodo, RefreshCw } from 'lucide-react'
|
import { Activity, AlertTriangle, Bird, RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Separator } from '@evobgp/ui/components/separator'
|
import { Separator } from '@evobgp/ui/components/separator'
|
||||||
@@ -22,7 +21,6 @@ import {
|
|||||||
monitoringHealthQueryOptions,
|
monitoringHealthQueryOptions,
|
||||||
monitoringReadyQueryOptions,
|
monitoringReadyQueryOptions,
|
||||||
monitoringVersionQueryOptions,
|
monitoringVersionQueryOptions,
|
||||||
type ReadyStatus,
|
|
||||||
type VersionInfo,
|
type VersionInfo,
|
||||||
} from '@/queries/monitoring'
|
} from '@/queries/monitoring'
|
||||||
import { networkBirdQueryOptions } from '@/queries/network'
|
import { networkBirdQueryOptions } from '@/queries/network'
|
||||||
@@ -84,7 +82,13 @@ function MonitoringComponent() {
|
|||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Мониторинг"
|
title="Мониторинг"
|
||||||
description="Состояние API, BGP и задач для диагностики инцидентов"
|
description={`Состояние API, BGP и задач для диагностики инцидентов${
|
||||||
|
versionText !== '—'
|
||||||
|
? ` · версия ${versionText}${
|
||||||
|
versionQ.data?.git_sha ? ` (${versionQ.data.git_sha.slice(0, 8)})` : ''
|
||||||
|
}`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
actions={
|
actions={
|
||||||
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
||||||
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
||||||
@@ -118,17 +122,6 @@ function MonitoringComponent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Alert className="border-muted bg-muted/30">
|
|
||||||
<Info className="size-4" />
|
|
||||||
<AlertTitle className="text-sm">
|
|
||||||
Версия API: {versionText}
|
|
||||||
{versionQ.data?.git_sha ? ` · ${versionQ.data.git_sha.slice(0, 8)}` : ''}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription className="text-xs">
|
|
||||||
{overallHint({ health: healthQ.data, jobsFailed: failed })}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<DataGridCard
|
<DataGridCard
|
||||||
title="Доступность и готовность"
|
title="Доступность и готовность"
|
||||||
@@ -225,37 +218,27 @@ function MonitoringComponent() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Короткая шпаргалка для triage</CardDescription>
|
<CardDescription>Короткая шпаргалка для triage</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent>
|
||||||
<Alert>
|
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||||
<HeartPulse className="size-4" />
|
<li>
|
||||||
<AlertTitle>API недоступен</AlertTitle>
|
<span className="font-medium text-foreground">API недоступен.</span> Если{' '}
|
||||||
<AlertDescription>
|
<code className="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс API и
|
||||||
Если <code className="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс
|
его логи.
|
||||||
API и его логи.
|
</li>
|
||||||
</AlertDescription>
|
<li>
|
||||||
</Alert>
|
<span className="font-medium text-foreground">Readiness не «Готов».</span> Сначала{' '}
|
||||||
<Alert>
|
<code className="text-xs">postgres</code>, затем <code className="text-xs">store</code>{' '}
|
||||||
<Database className="size-4" />
|
и <code className="text-xs">jobs</code> в checks.
|
||||||
<AlertTitle>Readiness не «Готов»</AlertTitle>
|
</li>
|
||||||
<AlertDescription>
|
<li>
|
||||||
Сначала <code className="text-xs">postgres</code>, затем{' '}
|
<span className="font-medium text-foreground">Низкий ratio BGP.</span> Проверьте{' '}
|
||||||
<code className="text-xs">store</code> и <code className="text-xs">jobs</code> в checks.
|
<code className="text-xs">/v1/bird/status</code>, затем состояние пиров в Сети.
|
||||||
</AlertDescription>
|
</li>
|
||||||
</Alert>
|
<li>
|
||||||
<Alert>
|
<span className="font-medium text-foreground">Ошибки задач.</span> Откройте Операции и
|
||||||
<Bird className="size-4" />
|
проверьте последние неуспешные jobs.
|
||||||
<AlertTitle>Низкий ratio BGP</AlertTitle>
|
</li>
|
||||||
<AlertDescription>
|
</ul>
|
||||||
Проверьте <code className="text-xs">/v1/bird/status</code>, затем состояние пиров в Сети.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
<Alert>
|
|
||||||
<ListTodo className="size-4" />
|
|
||||||
<AlertTitle>Ошибки задач</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Откройте Операции и проверьте последние неуспешные jobs.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,18 +248,11 @@ function MonitoringComponent() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">PostgreSQL</CardTitle>
|
<CardTitle className="text-base">PostgreSQL</CardTitle>
|
||||||
<CardDescription>Статус соединения и пул</CardDescription>
|
<CardDescription>
|
||||||
|
Статус соединения и пул. PostgreSQL отображается в readiness-проверке на вкладке «Система»
|
||||||
|
(check <code className="text-xs">postgres</code>).
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
|
||||||
<Alert>
|
|
||||||
<Database className="size-4" />
|
|
||||||
<AlertTitle>Статус готовности</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
PostgreSQL-соединение отображается в readiness-проверке на вкладке «Система» (check{' '}
|
|
||||||
<code className="text-xs">postgres</code>).
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -284,18 +260,11 @@ function MonitoringComponent() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Файловые логи</CardTitle>
|
<CardTitle className="text-base">Файловые логи</CardTitle>
|
||||||
<CardDescription>Логи API и pipeline</CardDescription>
|
<CardDescription>
|
||||||
|
Логи API и pipeline настраиваются переменной <code className="text-xs">EVOBGP_LOG_*</code> и
|
||||||
|
управляются в tenant-settings.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
|
||||||
<Alert>
|
|
||||||
<Info className="size-4" />
|
|
||||||
<AlertTitle>Логи на сервере</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Файловые логи настраиваются переменной <code className="text-xs">EVOBGP_LOG_*</code> и
|
|
||||||
управляются tenant-settings на странице «Настройки BIRD».
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</BadgeTabs>
|
</BadgeTabs>
|
||||||
@@ -317,18 +286,6 @@ function formatVersion(version?: VersionInfo | null): string {
|
|||||||
return version.version ?? version.app ?? '—'
|
return version.version ?? version.app ?? '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OverallInput {
|
|
||||||
health?: { ok?: boolean } | null
|
|
||||||
ready?: ReadyStatus | null
|
|
||||||
jobsFailed: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function overallHint(input: OverallInput): string {
|
|
||||||
if (!input.health?.ok) return 'API недоступен или возвращает ошибку'
|
|
||||||
if (input.jobsFailed > 0) return `Есть провальные задачи (${input.jobsFailed})`
|
|
||||||
return 'Все системы работают в штатном режиме'
|
|
||||||
}
|
|
||||||
|
|
||||||
function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
|
function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
|
||||||
if (!bird.birdc_configured) {
|
if (!bird.birdc_configured) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Info, RefreshCw } from 'lucide-react'
|
import { RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DashboardNetworkCapacityCard,
|
DashboardNetworkCapacityCard,
|
||||||
@@ -60,14 +59,6 @@ function NetworkComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>О сетевой конфигурации</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Вкладка «Обзор» — live-статус agent и BGP на CP и репликах. Apply и ревизии — на странице «Операции».
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<BadgeTabs
|
<BadgeTabs
|
||||||
value={search.tab}
|
value={search.tab}
|
||||||
onValueChange={(tab) =>
|
onValueChange={(tab) =>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Info, RefreshCw } from 'lucide-react'
|
import { RefreshCw } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
@@ -94,15 +93,6 @@ function OperationsComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Три раздела на одной странице</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<strong>Ревизии</strong> — история конфигов и откат; <strong>Сравнение</strong> — diff
|
|
||||||
префиксов; <strong>Задачи</strong> — ingest, apply, rollback.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
trigger={
|
trigger={
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { AlertTriangle, Clock, Info, ListTodo, RefreshCw } from 'lucide-react'
|
import { AlertTriangle, Clock, ListTodo, RefreshCw } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
import { DataGridCard } from '@/components/data-grid-shell'
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
@@ -87,16 +86,6 @@ function ScheduleComponent() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Как работает расписание</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Планировщик использует <code className="text-xs">refresh_interval_sec</code> и опционально{' '}
|
|
||||||
<code className="text-xs">cron_expr</code>. Ручной запуск —{' '}
|
|
||||||
<code className="text-xs">POST /v1/modules/{id}/refresh</code>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
||||||
|
|
||||||
<DataGridCard
|
<DataGridCard
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
@@ -13,7 +12,7 @@ import { SelectField } from '@/components/select-field'
|
|||||||
import { DEV_API_TOKEN, normalizeApiToken, setToken, TOKEN_STORAGE_KEY } from '@/lib/api-client'
|
import { DEV_API_TOKEN, normalizeApiToken, setToken, TOKEN_STORAGE_KEY } from '@/lib/api-client'
|
||||||
import { authKeys, authSessionQueryOptions } from '@/queries/auth'
|
import { authKeys, authSessionQueryOptions } from '@/queries/auth'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Info, Save } from 'lucide-react'
|
import { Save } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
@@ -70,16 +69,6 @@ function SettingsComponent() {
|
|||||||
description="Параметры интерфейса и подключения браузера к API."
|
description="Параметры интерфейса и подключения браузера к API."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Локальная разработка</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
При включённом demo-seed API принимает токен <code className="text-xs">dev</code> (роль{' '}
|
|
||||||
<code className="text-xs">operator</code>). Вводите только значение токена, без префикса{' '}
|
|
||||||
<code className="text-xs">Bearer</code> — он добавляется автоматически.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Подключение к API</CardTitle>
|
<CardTitle>Подключение к API</CardTitle>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Info, Save } from 'lucide-react'
|
import { Save } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
@@ -104,15 +103,6 @@ function TenantSettingsComponent() {
|
|||||||
description="Параметры control plane для текущего tenant (API /v1/settings)"
|
description="Параметры control plane для текущего tenant (API /v1/settings)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Operator-only</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Изменение значений через <code className="text-xs">PATCH /v1/settings</code> требует роли
|
|
||||||
operator. При отсутствии прав API вернёт 403.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<BadgeTabs
|
<BadgeTabs
|
||||||
value={search.tab}
|
value={search.tab}
|
||||||
onValueChange={(tab) =>
|
onValueChange={(tab) =>
|
||||||
@@ -137,14 +127,6 @@ function TenantSettingsComponent() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<Alert className="border-info/30 bg-info/5">
|
|
||||||
<Info className="text-info" />
|
|
||||||
<AlertTitle>Подстановка в конфиг</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Значения используются при генерации BIRD-конфигурации (router id, local AS, адреса).
|
|
||||||
Пиры и спикеры настраиваются в разделе «Сеть».
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
<QueryState
|
<QueryState
|
||||||
data={partitioned}
|
data={partitioned}
|
||||||
isLoading={settingsQ.isLoading}
|
isLoading={settingsQ.isLoading}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user