Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
128 lines
4.9 KiB
TypeScript
128 lines
4.9 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo, useState } from "react"
|
|
import { PageHeader } from "@/components/page-header"
|
|
import { DataTable } from "@/components/data-table"
|
|
import { FileImportDialog } from "@/components/file-import-dialog"
|
|
import { asns as mockAsns } from "@/lib/data"
|
|
import { Button } from "@/components/ui/button"
|
|
import { UploadIcon, DownloadIcon, PlusIcon, FilterIcon, LoaderCircleIcon } from "lucide-react"
|
|
import { useDataSource } from "@/lib/data-source"
|
|
import { useEvoBGP } from "@/lib/evobgp-context"
|
|
import { cn } from "@/lib/utils"
|
|
import { toast } from "sonner"
|
|
|
|
export default function AsnsPage() {
|
|
const { mode } = useDataSource()
|
|
const { enabled, snapshot, loading, error } = useEvoBGP()
|
|
const [importOpen, setImportOpen] = useState(false)
|
|
|
|
const useEvoCatalog = mode === "live" && enabled
|
|
|
|
const rows = useMemo(() => {
|
|
if (!useEvoCatalog) return mockAsns
|
|
if (loading && !snapshot) return []
|
|
return snapshot?.asns ?? []
|
|
}, [useEvoCatalog, loading, snapshot])
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<PageHeader
|
|
crumbs={[{ label: "Данные" }, { label: "ASN" }]}
|
|
actions={
|
|
<>
|
|
<Button variant="outline" size="sm" onClick={() => setImportOpen(true)}>
|
|
<UploadIcon className="size-4" />Импорт
|
|
</Button>
|
|
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
|
<Button size="sm"><PlusIcon className="size-4" />Добавить ASN</Button>
|
|
</>
|
|
}
|
|
/>
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">ASN</h1>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Автономные системы — источники маршрутов для BGP-фильтров
|
|
</p>
|
|
{useEvoCatalog && (
|
|
<p className={cn(
|
|
"text-xs mt-2 flex items-center gap-1.5",
|
|
error ? "text-destructive" : "text-muted-foreground",
|
|
)}>
|
|
{loading && <LoaderCircleIcon className="size-3.5 animate-spin shrink-0" />}
|
|
{error
|
|
? `EvoBGP: ${error}`
|
|
: snapshot?.fetchedAt
|
|
? `Источник: EvoBGP, обновлено ${new Date(snapshot.fetchedAt).toLocaleString("ru-RU")}`
|
|
: loading ? "Загрузка EvoBGP…" : "EvoBGP"}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<DataTable
|
|
data={rows}
|
|
isLoading={useEvoCatalog && loading && !snapshot}
|
|
searchPlaceholder="Поиск по ASN, имени, префиксам…"
|
|
searchKeys={["asn", "org", "prefixes"]}
|
|
columns={[
|
|
{
|
|
key: "asn",
|
|
label: "ASN",
|
|
render: (d) => <span className="font-mono font-semibold">{d.asn}</span>,
|
|
},
|
|
{
|
|
key: "org",
|
|
label: "Имя / организация",
|
|
render: (d) => (
|
|
<span className="font-medium max-w-[min(28rem,50vw)] truncate block" title={d.org}>
|
|
{d.org}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "prefixes",
|
|
label: "Префиксов",
|
|
render: (d) => <span className="font-mono tabular-nums">{d.prefixes.toLocaleString("ru")}</span>,
|
|
},
|
|
{
|
|
key: "filter",
|
|
label: "Фильтр",
|
|
render: (d) => (
|
|
<span className="inline-flex items-center gap-1 text-xs bg-muted rounded px-2 py-0.5">
|
|
<FilterIcon className="size-3 text-muted-foreground" />{d.filter}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "updated",
|
|
label: "Обновлён",
|
|
render: (d) => <span className="text-xs text-muted-foreground">{d.updated}</span>,
|
|
},
|
|
{
|
|
key: "enabled",
|
|
label: "Статус",
|
|
render: (d) => (
|
|
<span className={`text-xs font-medium ${d.enabled ? "text-emerald-600" : "text-muted-foreground"}`}>
|
|
{d.enabled ? "Активен" : "Отключён"}
|
|
</span>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<FileImportDialog
|
|
open={importOpen}
|
|
onOpenChange={setImportOpen}
|
|
title="Импорт ASN"
|
|
description="Загрузите CSV или JSON со списком автономных систем"
|
|
accept=".csv,.json,text/csv,application/json"
|
|
onImport={async (files) => {
|
|
toast.info(`Выбран файл: ${files[0]?.name ?? "—"}`)
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|