"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 { navigationGroups } from "@/lib/navigation" 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 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 navigationGroups.map((g) => ({ label: g.label, items: g.items.map((it) => ({ title: it.title, url: it.url, icon: React.createElement(it.icon, { "aria-hidden": true }), badge: mounted ? badgeFor(it.url) : undefined, })), })) }, [mounted, mode, liveCounts, mockBadges, evo.enabled, evo.snapshot]) return ( ) }