179 lines
7.3 KiB
TypeScript
179 lines
7.3 KiB
TypeScript
"use client"
|
||
|
||
import * as React from "react"
|
||
import { useTheme } from "@/components/theme-provider"
|
||
import { NavMain, type NavGroup } from "@/components/nav-main"
|
||
import { SearchIcon } from "lucide-react"
|
||
import {
|
||
Sidebar,
|
||
SidebarContent,
|
||
SidebarFooter,
|
||
SidebarHeader,
|
||
SidebarRail,
|
||
} from "@/components/ui/sidebar"
|
||
import {
|
||
RouterIcon,
|
||
LayoutDashboardIcon,
|
||
ActivityIcon,
|
||
MapIcon,
|
||
HeartPulseIcon,
|
||
GlobeIcon,
|
||
NetworkIcon,
|
||
LayersIcon,
|
||
TagIcon,
|
||
ServerIcon,
|
||
FilterIcon,
|
||
ShieldIcon,
|
||
HardDriveIcon,
|
||
RouteIcon,
|
||
GitForkIcon,
|
||
GitMergeIcon,
|
||
TerminalIcon,
|
||
SettingsIcon,
|
||
CableIcon,
|
||
ScanLineIcon,
|
||
BellIcon,
|
||
SunIcon,
|
||
MoonIcon,
|
||
ShieldCheckIcon,
|
||
BoxIcon,
|
||
BadgeCheckIcon,
|
||
} from "lucide-react"
|
||
|
||
// ─── Command palette trigger button ──────────────────────────────────────────
|
||
|
||
function CommandPaletteButton() {
|
||
function trigger() {
|
||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "k", ctrlKey: true, bubbles: true }))
|
||
}
|
||
return (
|
||
<button
|
||
onClick={trigger}
|
||
className="group-data-[collapsible=icon]:hidden mx-2 mb-1 flex items-center gap-2 px-2.5 py-1.5 rounded-md border border-sidebar-border/60 bg-sidebar-accent/30 hover:bg-sidebar-accent text-sidebar-foreground/50 hover:text-sidebar-foreground text-xs transition-colors w-[calc(100%-1rem)]"
|
||
>
|
||
<SearchIcon className="size-3 shrink-0" />
|
||
<span className="flex-1 text-left">Поиск…</span>
|
||
<span className="font-mono text-[10px] border border-sidebar-border/60 rounded px-1 py-0.5">⌘K</span>
|
||
</button>
|
||
)
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
const navGroups: NavGroup[] = [
|
||
{
|
||
label: "Обзор",
|
||
items: [
|
||
{ title: "Дашборд", url: "/dashboard", icon: <LayoutDashboardIcon /> },
|
||
{ title: "Трафик", url: "/traffic", icon: <ActivityIcon /> },
|
||
{ title: "Карта сети", url: "/network-map", icon: <MapIcon /> },
|
||
{ title: "Мониторинг", url: "/uptime", icon: <HeartPulseIcon />, badge: "8" },
|
||
],
|
||
},
|
||
{
|
||
label: "Данные",
|
||
items: [
|
||
{ title: "Домены", url: "/domains", icon: <GlobeIcon />, badge: "1.8к" },
|
||
{ title: "IP-диапазоны", url: "/ip-ranges", icon: <NetworkIcon />, badge: "4.2к" },
|
||
{ title: "ASN", url: "/asns", icon: <LayersIcon /> },
|
||
{ title: "Communities", url: "/communities", icon: <TagIcon /> },
|
||
],
|
||
},
|
||
{
|
||
label: "Управление",
|
||
items: [
|
||
{ title: "Серверы", url: "/servers", icon: <ServerIcon />, badge: "9" },
|
||
{ title: "Фильтры", url: "/filters", icon: <FilterIcon />, badge: "6" },
|
||
{ title: "Рекурсивные маршруты", url: "/recursive-routes", icon: <RouteIcon /> },
|
||
{ title: "Firewall", url: "/firewall", icon: <ShieldIcon /> },
|
||
{ title: "WireGuard", url: "/wireguard", icon: <ShieldCheckIcon />, badge: "4" },
|
||
{ title: "GRE-туннели", url: "/gre", icon: <CableIcon />, badge: "6" },
|
||
{ title: "VXLAN", url: "/vxlan", icon: <NetworkIcon /> },
|
||
{ title: "Контейнеры", url: "/containers", icon: <BoxIcon />, badge: "5" },
|
||
{ title: "Сертификаты", url: "/certificates", icon: <BadgeCheckIcon />, badge: "2" },
|
||
{ 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 />, badge: "12" },
|
||
{ title: "Диагностика", url: "/probes", icon: <ScanLineIcon /> },
|
||
{ title: "Терминал", url: "/terminal", icon: <TerminalIcon /> },
|
||
],
|
||
},
|
||
{
|
||
label: "Система",
|
||
items: [
|
||
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
|
||
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
|
||
],
|
||
},
|
||
]
|
||
|
||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||
const { resolvedTheme, setTheme } = useTheme()
|
||
const [mounted, setMounted] = React.useState(false)
|
||
React.useEffect(() => {
|
||
queueMicrotask(() => {
|
||
setMounted(true)
|
||
})
|
||
}, [])
|
||
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||
|
||
return (
|
||
<Sidebar collapsible="icon" {...props}>
|
||
<SidebarHeader>
|
||
<div className="flex items-center gap-2.5 px-2 py-1.5 overflow-hidden">
|
||
<div className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||
<RouterIcon className="size-4" />
|
||
</div>
|
||
<div className="flex flex-col leading-tight group-data-[collapsible=icon]:hidden">
|
||
<span className="font-semibold text-sidebar-foreground text-sm tracking-tight">RouterLists</span>
|
||
<span className="text-[10px] font-mono text-sidebar-foreground/40">v3.4.1</span>
|
||
</div>
|
||
</div>
|
||
{/* Command palette trigger */}
|
||
<CommandPaletteButton />
|
||
</SidebarHeader>
|
||
<SidebarContent>
|
||
<NavMain groups={navGroups} />
|
||
</SidebarContent>
|
||
<SidebarFooter>
|
||
<div className="flex items-center gap-2 px-2 py-2 group-data-[collapsible=icon]:justify-center">
|
||
{/* Avatar */}
|
||
<div className="size-7 rounded-full bg-gradient-to-br from-blue-500 to-violet-600 flex items-center justify-center text-white text-[10px] font-semibold shrink-0">
|
||
АК
|
||
</div>
|
||
{/* Name / email */}
|
||
<div className="flex flex-col leading-tight min-w-0 flex-1 group-data-[collapsible=icon]:hidden">
|
||
<span className="text-[13px] font-medium text-sidebar-foreground truncate">а.коротаев</span>
|
||
<span className="text-[11px] text-sidebar-foreground/50 truncate">admin@routerlists.io</span>
|
||
</div>
|
||
{/* Theme toggle — только после mount, иначе SSR и клиент расходятся (next-themes). */}
|
||
{mounted ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||
title={isDark ? "Светлая тема" : "Тёмная тема"}
|
||
className="shrink-0 size-6 flex items-center justify-center rounded-md text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors group-data-[collapsible=icon]:hidden"
|
||
>
|
||
{isDark
|
||
? <SunIcon className="size-3.5" />
|
||
: <MoonIcon className="size-3.5" />}
|
||
</button>
|
||
) : (
|
||
<span
|
||
className="shrink-0 size-6 group-data-[collapsible=icon]:hidden"
|
||
aria-hidden
|
||
/>
|
||
)}
|
||
</div>
|
||
</SidebarFooter>
|
||
<SidebarRail />
|
||
</Sidebar>
|
||
)
|
||
}
|