"use client" import * as React from "react" import { NavMain, type NavGroup } from "@/components/nav-main" import { AppSwitcher } from "@/components/app-switcher" import { NavUser } from "@/components/nav-user" import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, } from "@/components/ui/sidebar" import { LayoutDashboardIcon, ActivityIcon, ChartColumnIcon, MapIcon, HeartPulseIcon, GlobeIcon, NetworkIcon, LayersIcon, TagIcon, ServerIcon, FilterIcon, ShieldIcon, HardDriveIcon, RouteIcon, GitForkIcon, GitMergeIcon, TerminalIcon, SettingsIcon, DatabaseIcon, CableIcon, ScanLineIcon, BellIcon, ShieldCheckIcon, BoxIcon, BadgeCheckIcon, UsersIcon, LockIcon, } from "lucide-react" import { useDataSource } from "@/lib/data-source" import { useEvoBGP } from "@/lib/evobgp-context" import { requestJson } from "@/shared/api/http-client" import { formatSidebarBadgeCount, mockSidebarBadgesByUrl, type SidebarCountsDto, } from "@/lib/sidebar-badges" type NavItemBase = { title: string; url: string; icon: React.ReactNode } const navStructure: { label: string; items: NavItemBase[] }[] = [ { label: "Обзор", items: [ { title: "Дашборд", url: "/dashboard", icon: }, { title: "Трафик", url: "/traffic", icon: }, { title: "Статистика", url: "/statistics", icon: }, { title: "Карта сети", url: "/network-map", icon: }, { title: "Мониторинг", url: "/uptime", icon: }, ], }, { label: "Данные", items: [ { title: "Домены", url: "/domains", icon: }, { title: "IP-диапазоны", url: "/ip-ranges", icon: }, { title: "ASN", url: "/asns", icon: }, { title: "Communities", url: "/communities", icon: }, ], }, { label: "Управление", items: [ { title: "Серверы", url: "/servers", icon: }, { title: "Пользователи", url: "/users", icon: }, { title: "Фильтры", url: "/filters", icon: }, { title: "Рекурсивные маршруты", url: "/recursive-routes", icon: }, { title: "Firewall", url: "/firewall", icon: }, { title: "WireGuard", url: "/wireguard", icon: }, { title: "IPsec / IKEv2", url: "/ipsec", icon: }, { title: "GRE-туннели", url: "/gre", icon: }, { title: "VXLAN", url: "/vxlan", icon: }, { title: "Контейнеры", url: "/containers", icon: }, { title: "Сертификаты", url: "/certificates", icon: }, { title: "Бэкапы", url: "/backups", icon: }, ], }, { label: "Инструменты", items: [ { title: "Оптимизатор маршрутов", url: "/route-optimizer", icon: }, { title: "OSPF", url: "/ospf", icon: }, { title: "BGP", url: "/bgp", icon: }, { title: "Диагностика", url: "/probes", icon: }, { title: "Терминал", url: "/terminal", icon: }, ], }, { label: "Система", items: [ { title: "Оповещения", url: "/alerts", icon: }, { title: "Сбор данных", url: "/data-collection", icon: }, { title: "Релизы", url: "/releases", icon: }, { title: "Настройки", url: "/settings", icon: }, ], }, ] type LiveSidebarCounts = SidebarCountsDto & { greTunnels?: number certificates?: number wireguard?: number ipsec?: number users?: number bgpSessions?: number vxlan?: number containers?: number } export function AppSidebar({ ...props }: React.ComponentProps) { const { mode, backendUrl, prefsHydrated } = useDataSource() const evo = useEvoBGP() const [mounted, setMounted] = React.useState(false) const [liveCounts, setLiveCounts] = React.useState(null) const mockBadges = React.useMemo(() => mockSidebarBadgesByUrl(), []) React.useEffect(() => { queueMicrotask(() => { setMounted(true) }) }, []) React.useEffect(() => { if (!prefsHydrated || mode !== "live") { if (mode !== "live") setLiveCounts(null) return } let cancelled = false const load = async () => { try { const [cJson, gJson] = await Promise.all([ requestJson(backendUrl, "/api/sidebar-counts"), requestJson<{ tunnels?: unknown[] }>(backendUrl, "/api/filters/gre-tunnels").catch( () => ({ tunnels: [] as unknown[] }), ), ]) if (cancelled) return setLiveCounts({ ...cJson, greTunnels: (gJson.tunnels ?? []).length }) } catch { if (!cancelled) setLiveCounts(null) } } void load() const id = window.setInterval(load, 120_000) return () => { cancelled = true window.clearInterval(id) } }, [mode, backendUrl, prefsHydrated]) const navGroups = React.useMemo((): NavGroup[] => { function badgeFor(url: string): string | undefined { if (mode !== "live") return mockBadges[url] if (url === "/domains" || url === "/ip-ranges" || url === "/asns") { if (!evo.enabled) return undefined if (!evo.snapshot) return undefined if (url === "/domains") return formatSidebarBadgeCount(evo.snapshot.domains.length) if (url === "/ip-ranges") return formatSidebarBadgeCount(evo.snapshot.ipRanges.length) return formatSidebarBadgeCount(evo.snapshot.asns.length) } if (!liveCounts) return undefined if (url === "/servers") return formatSidebarBadgeCount(liveCounts.servers) if (url === "/users") return formatSidebarBadgeCount(liveCounts.users ?? 0) if (url === "/filters") return formatSidebarBadgeCount(liveCounts.filterRules) if (url === "/uptime") return formatSidebarBadgeCount(liveCounts.monitoringItems) if (url === "/gre") return formatSidebarBadgeCount(liveCounts.greTunnels ?? 0) if (url === "/certificates") return formatSidebarBadgeCount(liveCounts.certificates ?? 0) if (url === "/wireguard") return formatSidebarBadgeCount(liveCounts.wireguard ?? 0) if (url === "/ipsec") return formatSidebarBadgeCount(liveCounts.ipsec ?? 0) if (url === "/bgp") return formatSidebarBadgeCount(liveCounts.bgpSessions ?? 0) if (url === "/vxlan") return formatSidebarBadgeCount(liveCounts.vxlan ?? 0) if (url === "/containers") return formatSidebarBadgeCount(liveCounts.containers ?? 0) return undefined } return navStructure.map((g) => ({ label: g.label, items: g.items.map((it) => ({ title: it.title, url: it.url, icon: it.icon, badge: mounted ? badgeFor(it.url) : undefined, })), })) }, [mounted, mode, liveCounts, mockBadges, evo.enabled, evo.snapshot]) return ( ) }