Files
MikrotikManager/components/app-sidebar.tsx
T
Denozordec 883842636b
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m24s
Docker images / frontend-image (push) Successful in 2m6s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 7s
refactor(api): streamline API requests with requestJson and requestBlob functions
Replaced direct fetch calls with requestJson and requestBlob utility functions across multiple components for improved consistency and error handling. This change enhances the maintainability of the codebase by centralizing API request logic and ensuring uniform handling of authentication and response parsing.
2026-09-05 01:42:44 +07:00

201 lines
6.6 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 {
LayoutDashboardIcon,
ActivityIcon,
MapIcon,
HeartPulseIcon,
GlobeIcon,
NetworkIcon,
LayersIcon,
TagIcon,
ServerIcon,
FilterIcon,
ShieldIcon,
HardDriveIcon,
RouteIcon,
GitForkIcon,
GitMergeIcon,
TerminalIcon,
SettingsIcon,
DatabaseIcon,
CableIcon,
ScanLineIcon,
BellIcon,
ShieldCheckIcon,
BoxIcon,
BadgeCheckIcon,
} 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: <LayoutDashboardIcon /> },
{ title: "Трафик", url: "/traffic", icon: <ActivityIcon /> },
{ title: "Карта сети", url: "/network-map", icon: <MapIcon /> },
{ title: "Мониторинг", url: "/uptime", icon: <HeartPulseIcon /> },
],
},
{
label: "Данные",
items: [
{ title: "Домены", url: "/domains", icon: <GlobeIcon /> },
{ title: "IP-диапазоны", url: "/ip-ranges", icon: <NetworkIcon /> },
{ title: "ASN", url: "/asns", icon: <LayersIcon /> },
{ title: "Communities", url: "/communities", icon: <TagIcon /> },
],
},
{
label: "Управление",
items: [
{ title: "Серверы", url: "/servers", icon: <ServerIcon /> },
{ title: "Фильтры", url: "/filters", icon: <FilterIcon /> },
{ title: "Рекурсивные маршруты", url: "/recursive-routes", icon: <RouteIcon /> },
{ title: "Firewall", url: "/firewall", icon: <ShieldIcon /> },
{ title: "WireGuard", url: "/wireguard", icon: <ShieldCheckIcon /> },
{ title: "GRE-туннели", url: "/gre", icon: <CableIcon /> },
{ title: "VXLAN", url: "/vxlan", icon: <NetworkIcon /> },
{ title: "Контейнеры", url: "/containers", icon: <BoxIcon /> },
{ title: "Сертификаты", url: "/certificates", icon: <BadgeCheckIcon /> },
{ title: "Бэкапы", url: "/backups", icon: <HardDriveIcon /> },
],
},
{
label: "Инструменты",
items: [
{ title: "Оптимизатор маршрутов", url: "/route-optimizer", icon: <RouteIcon /> },
{ title: "OSPF", url: "/ospf", icon: <GitForkIcon /> },
{ title: "BGP", url: "/bgp", icon: <GitMergeIcon /> },
{ title: "Диагностика", url: "/probes", icon: <ScanLineIcon /> },
{ title: "Терминал", url: "/terminal", icon: <TerminalIcon /> },
],
},
{
label: "Система",
items: [
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
{ title: "Сбор данных", url: "/data-collection", icon: <DatabaseIcon /> },
{ title: "Релизы", url: "/releases", icon: <BadgeCheckIcon /> },
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
],
},
]
type LiveSidebarCounts = SidebarCountsDto & { greTunnels?: number; certificates?: 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 === "/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" || url === "/containers" || url === "/bgp") {
return undefined
}
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 (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<AppSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain groups={navGroups} />
</SidebarContent>
<SidebarFooter>
<NavUser />
</SidebarFooter>
</Sidebar>
)
}