Docker images / prepare-release (push) Successful in 4s
Docker images / backend-image (push) Failing after 2m36s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 55s
Docker images / publish-release (push) Skipped
Подключить App Switcher, NavUser, OpsPanel и AlertDialog вместо Card-shell. Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
6.7 KiB
TypeScript
209 lines
6.7 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 {
|
|
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 } = 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 (mode !== "live") {
|
|
setLiveCounts(null)
|
|
return
|
|
}
|
|
let cancelled = false
|
|
const load = async () => {
|
|
try {
|
|
const base = backendUrl.replace(/\/$/, "")
|
|
const [cRes, gRes] = await Promise.all([
|
|
fetch(`${base}/api/sidebar-counts`),
|
|
fetch(`${base}/api/filters/gre-tunnels`),
|
|
])
|
|
if (cancelled) return
|
|
if (!cRes.ok) {
|
|
setLiveCounts(null)
|
|
return
|
|
}
|
|
const cJson = (await cRes.json()) as SidebarCountsDto
|
|
let greN = 0
|
|
if (gRes.ok) {
|
|
const gJson = (await gRes.json()) as { tunnels?: unknown[] }
|
|
greN = (gJson.tunnels ?? []).length
|
|
}
|
|
setLiveCounts({ ...cJson, greTunnels: greN })
|
|
} catch {
|
|
if (!cancelled) setLiveCounts(null)
|
|
}
|
|
}
|
|
void load()
|
|
const id = window.setInterval(load, 120_000)
|
|
return () => {
|
|
cancelled = true
|
|
window.clearInterval(id)
|
|
}
|
|
}, [mode, backendUrl])
|
|
|
|
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>
|
|
)
|
|
}
|