"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 { can,ensureAuthConfig } from "@/lib/auth" import { useEvoBGP } from "@/lib/evobgp-context" import {getSidebarCounts,getSidebarGreCount} from "@/shared/api/sidebar-counts" import { formatSidebarBadgeCount, mockSidebarBadgesByUrl, type SidebarCountsDto, } from "@/lib/sidebar-badges" type LiveSidebarCounts = Partial & { 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 [snapshot, setSnapshot] = React.useState<{source:string;counts:LiveSidebarCounts}|null>(null) const source=JSON.stringify([mode,backendUrl]),liveCounts=snapshot?.source===source?snapshot.counts:null const countsAllowed=can("mm:dashboard:read"),greAllowed=can("mm:filters:read") const mockBadges = React.useMemo(() => mockSidebarBadgesByUrl(), []) React.useEffect(() => { let cancelled=false void ensureAuthConfig().then(()=>{if(!cancelled)setMounted(true)}) return()=>{cancelled=true} }, []) React.useEffect(() => { if(!mounted||!prefsHydrated||mode!=="live")return let cancelled=false,revision=0,controller:AbortController|null=null const load=async()=>{ controller?.abort();controller=new AbortController();const request=++revision,signal=AbortSignal.any([controller.signal,AbortSignal.timeout(30_000)]) const publish=(counts:LiveSidebarCounts)=>{if(cancelled||request!==revision)return;setSnapshot(previous=>({source,counts:{...(previous?.source===source?previous.counts:{}),...counts}}))} await Promise.allSettled([ can("mm:dashboard:read")?getSidebarCounts(backendUrl,signal).then(counts=>{if(can("mm:dashboard:read"))publish(counts)}):Promise.resolve(), can("mm:filters:read")?getSidebarGreCount(backendUrl,signal).then(greTunnels=>{if(can("mm:filters:read"))publish({greTunnels})}):Promise.resolve(), ]) } void load();const timer=window.setInterval(load,120_000) return()=>{cancelled=true;revision++;controller?.abort();window.clearInterval(timer)} },[mounted,mode,backendUrl,prefsHydrated,source,countsAllowed,greAllowed]) 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.settingsLoaded || !evo.enabled || !can("mm:settings:admin")) 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||(!countsAllowed&&url!=="/gre")) return undefined const permission=({"/servers":"mm:servers:read","/users":"mm:users:read","/filters":"mm:filters:read","/uptime":"mm:uptime:read","/gre":"mm:filters:read","/certificates":"mm:certificates:read","/wireguard":"mm:network:read","/ipsec":"mm:network:read","/bgp":"mm:bgp:read","/vxlan":"mm:network:read","/containers":"mm:dashboard:read"} as Record)[url] if(permission&&!can(permission))return undefined const format=(value:number|undefined)=>value===undefined?undefined:formatSidebarBadgeCount(value) if (url === "/servers") return format(liveCounts.servers) if (url === "/users") return format(liveCounts.users ) if (url === "/filters") return format(liveCounts.filterRules) if (url === "/uptime") return format(liveCounts.monitoringItems) if (url === "/gre") return format(liveCounts.greTunnels ) if (url === "/certificates") return format(liveCounts.certificates ) if (url === "/wireguard") return format(liveCounts.wireguard ) if (url === "/ipsec") return format(liveCounts.ipsec ) if (url === "/bgp") return format(liveCounts.bgpSessions ) if (url === "/vxlan") return format(liveCounts.vxlan ) if (url === "/containers") return format(liveCounts.containers ) 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, countsAllowed, mockBadges, evo.settingsLoaded, evo.enabled, evo.snapshot]) return ( ) }