123 lines
5.4 KiB
TypeScript
123 lines
5.4 KiB
TypeScript
"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<SidebarCountsDto> & {
|
|
greTunnels?: number
|
|
certificates?: number
|
|
wireguard?: number
|
|
ipsec?: number
|
|
users?: number
|
|
bgpSessions?: number
|
|
vxlan?: number
|
|
containers?: number
|
|
}
|
|
|
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
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<string,string>)[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 (
|
|
<Sidebar collapsible="icon" {...props}>
|
|
<SidebarHeader>
|
|
<AppSwitcher />
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<NavMain groups={navGroups} />
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<NavUser />
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
)
|
|
}
|