Files
MikrotikManager/components/app-sidebar.tsx
T

130 lines
4.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 { 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<typeof Sidebar>) {
const { mode, backendUrl, prefsHydrated } = useDataSource()
const evo = useEvoBGP()
const [mounted, setMounted] = React.useState(false)
const [liveCounts, setLiveCounts] = React.useState<LiveSidebarCounts | null>(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<SidebarCountsDto>(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 (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<AppSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain groups={navGroups} />
</SidebarContent>
<SidebarFooter>
<NavUser />
</SidebarFooter>
</Sidebar>
)
}