"use client" import { useState, useMemo, useEffect } from "react" import { PageHeader } from "@/components/page-header" import { DataPageCard } from "@/components/data-page-card" import { CommunitiesDataGrid, type CommunityRow, TYPE_LABELS, ACTION_LABELS, ACTION_COLOR, } from "@/components/data-grids/communities-data-grid" import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card" import { Button } from "@/components/ui/button" 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 { useEvoBGP } from "@/lib/evobgp-context" // ─── types ──────────────────────────────────────────────────────────────────── type Community = CommunityRow type CommType = CommunityRow["type"] // ─── mock data ──────────────────────────────────────────────────────────────── const COMMUNITIES: Community[] = [ { id: "c1", value: "65001:100", name: "youtube-bypass", description: "Пометка для трафика YouTube — маршрутизация через SPB/FRA exit nodes", type: "standard", filterIds: ["f1"], serverCount: 4, prefixCount: 842, action: "local-pref", actionValue: 200, enabled: true, }, { id: "c2", value: "65001:200", name: "streaming-eu", description: "EU-стриминг (Netflix, Twitch) — выход через FRA/AMS", type: "standard", filterIds: ["f1", "f2"], serverCount: 3, prefixCount: 614, action: "local-pref", actionValue: 180, enabled: true, }, { id: "c3", value: "65001:300", name: "cdn-bypass", description: "Обход CDN-провайдеров через прямые пиринговые IX-точки", type: "standard", filterIds: ["f3"], serverCount: 5, prefixCount: 4218, action: "local-pref", actionValue: 210, enabled: true, }, { id: "c4", value: "65002:100", name: "cdn-secondary", description: "Резервный CDN-путь при деградации основного", type: "standard", filterIds: ["f3"], serverCount: 5, prefixCount: 1842, action: "metric", actionValue: 50, enabled: true, }, { id: "c5", value: "65001:400", name: "office-direct", description: "Office 365 / Teams — прямой выход без туннелирования", type: "standard", filterIds: ["f5"], serverCount: 6, prefixCount: 882, action: "permit", enabled: true, }, { id: "c6", value: "65001:500", name: "gaming-ll", description: "Low-latency gaming — приоритет по минимальному RTT (Discord, Steam)", type: "standard", filterIds: ["f6"], serverCount: 4, prefixCount: 1212, action: "local-pref", actionValue: 250, enabled: true, }, { id: "c7", value: "65002:200", name: "gaming-ll-backup", description: "Резервный путь для gaming-low-latency", type: "standard", filterIds: ["f6"], serverCount: 4, prefixCount: 412, action: "metric", actionValue: 80, enabled: true, }, { id: "c8", value: "65007:999", name: "school-block", description: "Блокировка соцсетей для школьного сегмента LAN", type: "standard", filterIds: ["f4"], serverCount: 2, prefixCount: 412, action: "deny", enabled: false, }, { id: "c9", value: "no-export", name: "no-export", description: "Не объявлять маршрут за пределы AS (RFC 1997)", type: "no-export", filterIds: [], serverCount: 1, prefixCount: 0, action: "permit", enabled: true, }, { id: "c10", value: "no-advertise", name: "no-advertise", description: "Не передавать маршрут ни одному BGP-пиру (RFC 1997)", type: "no-advertise", filterIds: [], serverCount: 1, prefixCount: 0, action: "permit", enabled: true, }, ] // ─── page ───────────────────────────────────────────────────────────────────── export default function CommunitiesPage() { const { mode } = useDataSource() const { enabled, snapshot, loading, error } = useEvoBGP() const useEvoCatalog = mode === "live" && enabled const [search, setSearch] = useState("") const [typeFilter, setTypeFilter] = useState("all") const [copied, setCopied] = useState(null) const [selected, setSelected] = useState(null) const listData = useMemo((): Community[] => { if (!useEvoCatalog) return COMMUNITIES if (loading && !snapshot) return [] return snapshot?.communities ?? [] }, [useEvoCatalog, loading, snapshot]) useEffect(() => { setSelected(null) }, [useEvoCatalog]) const filtered = useMemo(() => { const q = search.toLowerCase() return listData.filter(c => { const matchQ = !q || c.value.toLowerCase().includes(q) || c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) const matchT = typeFilter === "all" || c.type === typeFilter return matchQ && matchT }) }, [search, typeFilter, listData]) const handleCopy = (value: string) => { navigator.clipboard.writeText(value).catch(() => {}) setCopied(value) setTimeout(() => setCopied(null), 1500) } const filterName = (id: string) => filters.find(f => f.id === id)?.name ?? id return (
Добавить } />
{useEvoCatalog && (

{loading && } {error ? `EvoBGP: ${error}` : snapshot?.fetchedAt ? `Источник: EvoBGP, обновлено ${new Date(snapshot.fetchedAt).toLocaleString("ru-RU")}` : loading ? "Загрузка EvoBGP…" : "EvoBGP"}

)} {/* ── summary ── */}
{[ { label: "Всего communities", value: String(listData.length) }, { label: "Активных", value: String(listData.filter(c => c.enabled).length) }, { label: "Стандартных", value: String(listData.filter(c => c.type === "standard").length) }, { label: "Использует фильтры",value: String(new Set(listData.flatMap(c => c.filterIds)).size) }, ].map(({ label, value }) => (

{label}

{value}

))}
{/* ── main table ── */}
{/* toolbar */}
setSearch(e.target.value)} />
{(["all", "standard", "no-export", "no-advertise", "custom"] as const).map(t => ( ))}
{/* list */}
{/* ── detail panel ── */} {selected ? (
{selected.enabled ? "Активен" : "Выключен"}
{selected.value} {selected.name}

{selected.description}

{[ ["Тип", TYPE_LABELS[selected.type]], ["Действие", `${ACTION_LABELS[selected.action]}${selected.actionValue !== undefined ? ` ${selected.actionValue}` : ""}`], ["Маршрутов", selected.prefixCount.toLocaleString("ru-RU")], ["Серверов", selected.serverCount.toLocaleString("ru-RU")], ].map(([k, v]) => (
{k} {v}
))}
{selected.filterIds.length > 0 && (

Использующие фильтры

{selected.filterIds.map(fid => ( {filterName(fid)} ))}
)}
) : (

Выберите community
для просмотра деталей

)}
) }