fix(evolution): исключить моковые данные из живого каталога

This commit is contained in:
Denozordec committed 2026-10-03 14:08:44 +07:00
1 parent 118cd31d65
commit d272ef57f0
17 files changed
+155 -136

No files matched your search

+12 -27
View File
@@ -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>
+24 -39
View File
@@ -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
View File
@@ -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>
+10 -5
View File
@@ -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 (текущий + цели копирования) */
+13 -28
View File
@@ -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>