fix(evolution): исключить моковые данные из живого каталога
This commit is contained in:
1 parent
118cd31d65
commit
d272ef57f0
17 files changed
+155
-136
No files matched your search
+12
-27
@@ -8,25 +8,22 @@ import { AsnsDataGrid } from "@/components/data-grids/asns-data-grid"
|
||||
import { FileImportDialog } from "@/components/file-import-dialog"
|
||||
import { asns as mockAsns } from "@/lib/data"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon } from "lucide-react"
|
||||
import { useEvoCatalogView, EvoBgpCatalogStatus, EvoBgpCatalogUnavailable } from "@/components/settings/evobgp-catalog-status"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function AsnsPage() {
|
||||
const { mode } = useDataSource()
|
||||
const { enabled, snapshot, loading, error } = useEvoBGP()
|
||||
const { mode, snapshot, loading, known } = useEvoCatalogView()
|
||||
const [importOpen, setImportOpen] = useState(false)
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
const useEvoCatalog = mode === "live" && enabled
|
||||
const useEvoCatalog = mode === "live"
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!useEvoCatalog) return mockAsns
|
||||
if (mode === "mock") return mockAsns
|
||||
if (loading && !snapshot) return []
|
||||
return snapshot?.asns ?? []
|
||||
}, [useEvoCatalog, loading, snapshot])
|
||||
}, [mode, loading, snapshot])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return rows
|
||||
@@ -43,8 +40,7 @@ export default function AsnsPage() {
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Данные" }, { label: "ASN" }]}
|
||||
actions={
|
||||
<>
|
||||
actions={mode === "live" ? null : <>
|
||||
<Button variant="outline" size="sm" onClick={() => setImportOpen(true)}>
|
||||
<UploadIcon className="size-4" />Импорт
|
||||
</Button>
|
||||
@@ -60,32 +56,21 @@ export default function AsnsPage() {
|
||||
<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 className="mt-3"><EvoBgpCatalogStatus /></div>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Поиск по ASN, имени, префиксам…"
|
||||
countLabel={`${filtered.length} ASN`}
|
||||
countLabel={known ? `${filtered.length} ASN` : "Количество: —"}
|
||||
/>
|
||||
<AsnsDataGrid
|
||||
{known || loading ? <AsnsDataGrid
|
||||
asns={filtered}
|
||||
isLoading={useEvoCatalog && loading && !snapshot}
|
||||
pagination={useEvoCatalog}
|
||||
/>
|
||||
emptyDescription={search ? "Измените поисковый запрос" : useEvoCatalog ? "В подтверждённом каталоге нет записей этого типа" : undefined}
|
||||
/> : <EvoBgpCatalogUnavailable />}
|
||||
</DataPageCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo, useEffect } from "react"
|
||||
import { useState, useMemo } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type CommunityRow,
|
||||
TYPE_LABELS,
|
||||
ACTION_LABELS,
|
||||
ACTION_COLOR,
|
||||
} from "@/components/data-grids/communities-data-grid"
|
||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
@@ -18,11 +17,10 @@ import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
PlusIcon, SearchIcon, TagIcon, FilterIcon,
|
||||
CopyIcon, CheckIcon, TrashIcon, PencilIcon,
|
||||
LoaderCircleIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { filters } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useEvoCatalogView, EvoBgpCatalogStatus, EvoBgpCatalogUnavailable } from "@/components/settings/evobgp-catalog-status"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
@@ -98,24 +96,24 @@ const COMMUNITIES: Community[] = [
|
||||
// ─── page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function CommunitiesPage() {
|
||||
const { mode } = useDataSource()
|
||||
const { enabled, snapshot, loading, error } = useEvoBGP()
|
||||
const useEvoCatalog = mode === "live" && enabled
|
||||
const { sourceKey } = useEvoBGP()
|
||||
return <CommunitiesContent key={sourceKey} />
|
||||
}
|
||||
function CommunitiesContent() {
|
||||
const { mode, snapshot, loading, known } = useEvoCatalogView()
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
const [typeFilter, setTypeFilter] = useState<CommType | "all">("all")
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
const [selected, setSelected] = useState<Community | null>(null)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
const listData = useMemo((): Community[] => {
|
||||
if (!useEvoCatalog) return COMMUNITIES
|
||||
if (mode === "mock") return COMMUNITIES
|
||||
if (loading && !snapshot) return []
|
||||
return snapshot?.communities ?? []
|
||||
}, [useEvoCatalog, loading, snapshot])
|
||||
}, [mode, loading, snapshot])
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(null)
|
||||
}, [useEvoCatalog])
|
||||
const selected = known ? listData.find(row => row.id === selectedId) ?? null : null
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.toLowerCase()
|
||||
@@ -138,8 +136,7 @@ export default function CommunitiesPage() {
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Данные" }, { label: "Communities" }]}
|
||||
actions={
|
||||
<Button size="sm">
|
||||
actions={mode === "live" ? null : <Button size="sm">
|
||||
<PlusIcon className="size-4" />Добавить
|
||||
</Button>
|
||||
}
|
||||
@@ -147,19 +144,7 @@ export default function CommunitiesPage() {
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex flex-col gap-5">
|
||||
{useEvoCatalog && (
|
||||
<p className={cn(
|
||||
"text-xs flex items-center gap-1.5 -mt-1",
|
||||
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>
|
||||
)}
|
||||
<EvoBgpCatalogStatus />
|
||||
|
||||
<KpiStatGrid
|
||||
aria-label="Сводка communities"
|
||||
@@ -167,35 +152,35 @@ export default function CommunitiesPage() {
|
||||
{
|
||||
id: "all",
|
||||
label: "Всего communities",
|
||||
value: String(listData.length),
|
||||
value: known ? String(listData.length) : "—",
|
||||
icon: <TagIcon className="size-4" />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
label: "Активных",
|
||||
value: String(listData.filter((c) => c.enabled).length),
|
||||
value: known ? String(listData.filter((c) => c.enabled).length) : "—",
|
||||
icon: <CheckIcon className="size-4" />,
|
||||
iconClassName: "text-success",
|
||||
},
|
||||
{
|
||||
id: "standard",
|
||||
label: "Стандартных",
|
||||
value: String(listData.filter((c) => c.type === "standard").length),
|
||||
value: known ? String(listData.filter((c) => c.type === "standard").length) : "—",
|
||||
icon: <TagIcon className="size-4" />,
|
||||
iconClassName: "text-info",
|
||||
},
|
||||
{
|
||||
id: "filters",
|
||||
label: "Использует фильтры",
|
||||
value: String(new Set(listData.flatMap((c) => c.filterIds)).size),
|
||||
value: mode === "mock" ? String(new Set(listData.flatMap((c) => c.filterIds)).size) : "—",
|
||||
icon: <FilterIcon className="size-4" />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-[1fr_320px] gap-5">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_320px] gap-5">
|
||||
{/* ── main table ── */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* toolbar */}
|
||||
@@ -204,7 +189,7 @@ export default function CommunitiesPage() {
|
||||
<SearchIcon className="absolute left-2.5 top-2 size-4 text-muted-foreground" />
|
||||
<Input className="pl-8 h-8 text-sm" placeholder="Поиск community…" value={search} onChange={e => setSearch(e.target.value)} />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(["all", "standard", "no-export", "no-advertise", "custom"] as const).map(t => (
|
||||
<Button key={t} size="sm" variant={typeFilter === t ? "default" : "outline"}
|
||||
className="h-8 text-xs px-2.5" onClick={() => setTypeFilter(t)}>
|
||||
@@ -216,13 +201,13 @@ export default function CommunitiesPage() {
|
||||
|
||||
{/* list */}
|
||||
<DataPageCard>
|
||||
<CommunitiesDataGrid
|
||||
{known ? <CommunitiesDataGrid
|
||||
communities={filtered}
|
||||
selectedId={selected?.id}
|
||||
copiedValue={copied}
|
||||
onSelect={setSelected}
|
||||
onSelect={row => setSelectedId(row.id)}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
/> : <EvoBgpCatalogUnavailable />}
|
||||
</DataPageCard>
|
||||
</div>
|
||||
|
||||
@@ -234,8 +219,8 @@ export default function CommunitiesPage() {
|
||||
description={selected.name}
|
||||
headerRight={
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<Button size="sm" variant="ghost" className="size-7 p-0"><PencilIcon className="size-3.5" /></Button>
|
||||
<Button size="sm" variant="ghost" className="size-7 p-0 text-destructive hover:text-destructive"><TrashIcon className="size-3.5" /></Button>
|
||||
<Button size="sm" variant="ghost" aria-label="Изменить community" disabled className="size-7 p-0"><PencilIcon className="size-3.5" /></Button>
|
||||
<Button size="sm" variant="ghost" aria-label="Удалить community" disabled className="size-7 p-0 text-destructive hover:text-destructive"><TrashIcon className="size-3.5" /></Button>
|
||||
</div>
|
||||
}
|
||||
contentClassName="flex flex-col gap-4 text-sm px-5 pb-5"
|
||||
|
||||
+13
-28
@@ -8,25 +8,22 @@ import { DomainsDataGrid } from "@/components/data-grids/domains-data-grid"
|
||||
import { FileImportDialog } from "@/components/file-import-dialog"
|
||||
import { domains as mockDomains } from "@/lib/data"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon } from "lucide-react"
|
||||
import { useEvoCatalogView, EvoBgpCatalogStatus, EvoBgpCatalogUnavailable } from "@/components/settings/evobgp-catalog-status"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function DomainsPage() {
|
||||
const { mode } = useDataSource()
|
||||
const { enabled, snapshot, loading, error } = useEvoBGP()
|
||||
const { mode, snapshot, loading, known } = useEvoCatalogView()
|
||||
const [importOpen, setImportOpen] = useState(false)
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
const useEvoCatalog = mode === "live" && enabled
|
||||
const useEvoCatalog = mode === "live"
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!useEvoCatalog) return mockDomains
|
||||
if (mode === "mock") return mockDomains
|
||||
if (loading && !snapshot) return []
|
||||
return snapshot?.domains ?? []
|
||||
}, [useEvoCatalog, loading, snapshot])
|
||||
}, [mode, loading, snapshot])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return rows
|
||||
@@ -43,8 +40,7 @@ export default function DomainsPage() {
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Данные" }, { label: "Домены" }]}
|
||||
actions={
|
||||
<>
|
||||
actions={mode === "live" ? null : <>
|
||||
<Button variant="outline" size="sm" onClick={() => setImportOpen(true)}>
|
||||
<UploadIcon className="size-4" />Импорт
|
||||
</Button>
|
||||
@@ -58,34 +54,23 @@ export default function DomainsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Домены</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Отслеживаемые домены для BGP-фильтрации — синхронизируются в address-list MikroTik
|
||||
Домены из каталога для 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 className="mt-3"><EvoBgpCatalogStatus /></div>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Поиск по домену…"
|
||||
countLabel={`${filtered.length} доменов`}
|
||||
countLabel={known ? `${filtered.length} доменов` : "Количество: —"}
|
||||
/>
|
||||
<DomainsDataGrid
|
||||
{known || loading ? <DomainsDataGrid
|
||||
domains={filtered}
|
||||
isLoading={useEvoCatalog && loading && !snapshot}
|
||||
pagination={useEvoCatalog}
|
||||
/>
|
||||
emptyDescription={search ? "Измените поисковый запрос" : useEvoCatalog ? "В подтверждённом каталоге нет записей этого типа" : undefined}
|
||||
/> : <EvoBgpCatalogUnavailable />}
|
||||
</DataPageCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
|
||||
} from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { can } from "@/lib/auth"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
import {
|
||||
PlusIcon, SearchIcon, XIcon, CheckIcon, AlertCircleIcon,
|
||||
@@ -1280,9 +1281,9 @@ function FiltersPage() {
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const communityNameMap = useMemo(() => {
|
||||
if (mode === "live" && evo.enabled) {
|
||||
if (mode === "live") {
|
||||
const m: Record<string, string> = {}
|
||||
for (const c of evo.snapshot?.communities ?? []) {
|
||||
for (const c of evo.enabled && can("mm:settings:admin") ? evo.snapshot?.communities ?? [] : []) {
|
||||
const v = (c.value ?? "").trim()
|
||||
if (!v) continue
|
||||
m[v] = c.name?.trim() || v
|
||||
@@ -1290,7 +1291,7 @@ function FiltersPage() {
|
||||
return m
|
||||
}
|
||||
return { ...COMMUNITY_NAMES }
|
||||
}, [mode, evo.enabled, evo.snapshot?.communities, evo.snapshot?.fetchedAt])
|
||||
}, [mode, evo.enabled, evo.snapshot?.communities])
|
||||
|
||||
const communitySuggestions = useMemo(
|
||||
() => Object.keys(communityNameMap).sort((a, b) => a.localeCompare(b)),
|
||||
@@ -1298,10 +1299,14 @@ function FiltersPage() {
|
||||
)
|
||||
|
||||
const evoCommunityHint = useMemo(() => {
|
||||
if (mode !== "live" || !evo.enabled) return null
|
||||
if (mode !== "live") return null
|
||||
if (!can("mm:settings:admin")) return "Справочник EvoBGP недоступен по правам; значение community можно ввести вручную"
|
||||
if (!evo.settingsLoaded) return "Настройки справочника EvoBGP неизвестны; значение community можно ввести вручную"
|
||||
if (!evo.enabled) return "Интеграция EvoBGP выключена; значение community можно ввести вручную"
|
||||
if (evo.error) return "Справочник EvoBGP не обновлён: " + evo.error
|
||||
if (evo.loading) return "Загрузка справочника communities из EvoBGP…"
|
||||
return null
|
||||
}, [mode, evo.enabled, evo.loading])
|
||||
}, [mode, evo.settingsLoaded, evo.enabled, evo.error, evo.loading])
|
||||
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
/** GRE с роутера только по запрошенным serverId (текущий + цели копирования) */
|
||||
|
||||
@@ -8,25 +8,22 @@ import { IpRangesDataGrid } from "@/components/data-grids/ip-ranges-data-grid"
|
||||
import { FileImportDialog } from "@/components/file-import-dialog"
|
||||
import { ipRanges as mockIpRanges } from "@/lib/data"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { UploadIcon, DownloadIcon, PlusIcon } from "lucide-react"
|
||||
import { useEvoCatalogView, EvoBgpCatalogStatus, EvoBgpCatalogUnavailable } from "@/components/settings/evobgp-catalog-status"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function IpRangesPage() {
|
||||
const { mode } = useDataSource()
|
||||
const { enabled, snapshot, loading, error } = useEvoBGP()
|
||||
const { mode, snapshot, loading, known } = useEvoCatalogView()
|
||||
const [importOpen, setImportOpen] = useState(false)
|
||||
const [search, setSearch] = useState("")
|
||||
|
||||
const useEvoCatalog = mode === "live" && enabled
|
||||
const useEvoCatalog = mode === "live"
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!useEvoCatalog) return mockIpRanges
|
||||
if (mode === "mock") return mockIpRanges
|
||||
if (loading && !snapshot) return []
|
||||
return snapshot?.ipRanges ?? []
|
||||
}, [useEvoCatalog, loading, snapshot])
|
||||
}, [mode, loading, snapshot])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return rows
|
||||
@@ -44,8 +41,7 @@ export default function IpRangesPage() {
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Данные" }, { label: "IP-диапазоны" }]}
|
||||
actions={
|
||||
<>
|
||||
actions={mode === "live" ? null : <>
|
||||
<Button variant="outline" size="sm" onClick={() => setImportOpen(true)}>
|
||||
<UploadIcon className="size-4" />Импорт
|
||||
</Button>
|
||||
@@ -59,34 +55,23 @@ export default function IpRangesPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">IP-диапазоны</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
CIDR-блоки, отправляемые в address-list MikroTik по всем настроенным фильтрам
|
||||
CIDR-блоки из каталога для сетевых фильтров
|
||||
</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 className="mt-3"><EvoBgpCatalogStatus /></div>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Поиск по CIDR, ASN…"
|
||||
countLabel={`${filtered.length} диапазонов`}
|
||||
countLabel={known ? `${filtered.length} диапазонов` : "Количество: —"}
|
||||
/>
|
||||
<IpRangesDataGrid
|
||||
{known || loading ? <IpRangesDataGrid
|
||||
ipRanges={filtered}
|
||||
isLoading={useEvoCatalog && loading && !snapshot}
|
||||
pagination={useEvoCatalog}
|
||||
/>
|
||||
emptyDescription={search ? "Измените поисковый запрос" : useEvoCatalog ? "В подтверждённом каталоге нет записей этого типа" : undefined}
|
||||
/> : <EvoBgpCatalogUnavailable />}
|
||||
</DataPageCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user