"use client" import { useEffect, useState, useRef, useMemo } from "react" import { useRouter, usePathname } from "next/navigation" import { cn } from "@/lib/utils" import { SearchIcon, LayoutDashboardIcon, ActivityIcon, MapIcon, HeartPulseIcon, GlobeIcon, NetworkIcon, LayersIcon, TagIcon, ServerIcon, FilterIcon, ShieldIcon, ShieldCheckIcon, CableIcon, BoxIcon, BadgeCheckIcon, HardDriveIcon, RouteIcon, GitForkIcon, GitMergeIcon, ScanLineIcon, TerminalIcon, BellIcon, SettingsIcon, DatabaseIcon, } from "lucide-react" // ─── Command item definition ────────────────────────────────────────────────── interface CommandItem { id: string title: string subtitle?: string group: string url: string icon: React.ReactNode keywords?: string[] } const ALL_ITEMS: CommandItem[] = [ // Обзор { id: "dashboard", title: "Дашборд", group: "Обзор", url: "/dashboard", icon: , keywords: ["главная","home","overview"] }, { id: "traffic", title: "Трафик", group: "Обзор", url: "/traffic", icon: , keywords: ["bandwidth","traffic","клиенты","интерфейсы"] }, { id: "network-map", title: "Карта сети", group: "Обзор", url: "/network-map", icon: , keywords: ["topology","топология","map"] }, { id: "uptime", title: "Мониторинг / Uptime", group: "Обзор", url: "/uptime", icon: , keywords: ["ping","uptime","мониторинг","проверка"] }, // Данные { id: "domains", title: "Домены", group: "Данные", url: "/domains", icon: , keywords: ["domain","dns","сайты"] }, { id: "ip-ranges", title: "IP-диапазоны", group: "Данные", url: "/ip-ranges", icon: , keywords: ["cidr","subnet","подсети","префиксы"] }, { id: "asns", title: "ASN", group: "Данные", url: "/asns", icon: , keywords: ["autonomous system","автономная система"] }, { id: "communities", title: "BGP Communities", group: "Данные", url: "/communities", icon: , keywords: ["community","bgp","теги"] }, // Управление { id: "servers", title: "Серверы", group: "Управление", url: "/servers", icon: , keywords: ["router","mikrotik","сервер","routeros"] }, { id: "filters", title: "Фильтры", group: "Управление", url: "/filters", icon: , keywords: ["filter","routing","маршрутизация"] }, { id: "recursive-routes", title: "Рекурсивные маршруты", group: "Управление", url: "/recursive-routes", icon: , keywords: ["recursive","route","static","маршруты"] }, { id: "firewall", title: "Firewall", group: "Управление", url: "/firewall", icon: , keywords: ["rules","правила","брандмауэр","acl"] }, { id: "wireguard", title: "WireGuard", group: "Управление", url: "/wireguard", icon: , keywords: ["vpn","tunnel","туннель","wg"] }, { id: "gre", title: "GRE-туннели", group: "Управление", url: "/gre", icon: , keywords: ["gre","ipsec","tunnel","туннель"] }, { id: "vxlan", title: "VXLAN", group: "Управление", url: "/vxlan", icon: , keywords: ["overlay","vni","vtep","l2"] }, { id: "containers", title: "Контейнеры", group: "Управление", url: "/containers", icon: , keywords: ["docker","container","образ","image"] }, { id: "certificates", title: "Сертификаты", group: "Управление", url: "/certificates", icon: , keywords: ["ssl","tls","cert","pki","x509"] }, { id: "backups", title: "Бэкапы", group: "Управление", url: "/backups", icon: , keywords: ["backup","backup","резервная копия"] }, // Инструменты { id: "route-optimizer", title: "Оптимизатор маршрутов", group: "Инструменты", url: "/route-optimizer", icon: , keywords: ["routing","wan","failover","переключение"] }, { id: "ospf", title: "OSPF", group: "Инструменты", url: "/ospf", icon: , keywords: ["ospf","igp","neighbors","lsa"] }, { id: "bgp", title: "BGP", group: "Инструменты", url: "/bgp", icon: , keywords: ["bgp","ebgp","ibgp","сессии","peers"] }, { id: "probes", title: "Диагностика", group: "Инструменты", url: "/probes", icon: , keywords: ["probe","ping","traceroute","диагностика"] }, { id: "terminal", title: "Терминал", group: "Инструменты", url: "/terminal", icon: , keywords: ["terminal","cli","ssh","консоль"] }, // Система { id: "alerts", title: "Оповещения", group: "Система", url: "/alerts", icon: , keywords: ["alert","notification","уведомление"] }, { id: "data-collection", title: "Сбор данных", group: "Система", url: "/data-collection", icon: , keywords: ["scheduler","планировщик","коллектор","uptime","трафик","журнал","прогон"] }, { id: "releases", title: "Релизы", group: "Система", url: "/releases", icon: , keywords: ["release","version","версия","changelog","релиз"] }, { id: "settings", title: "Настройки", group: "Система", url: "/settings", icon: , keywords: ["settings","config","конфигурация"] }, ] // ─── Scoring / filtering ────────────────────────────────────────────────────── function score(item: CommandItem, q: string): number { const t = item.title.toLowerCase() const kws = (item.keywords ?? []).join(" ").toLowerCase() if (t === q) return 100 if (t.startsWith(q)) return 90 if (t.includes(q)) return 70 if (kws.includes(q)) return 50 return 0 } function filterItems(q: string): CommandItem[] { if (!q.trim()) return ALL_ITEMS const lq = q.trim().toLowerCase() return ALL_ITEMS .map(item => ({ item, s: score(item, lq) })) .filter(x => x.s > 0) .sort((a, b) => b.s - a.s) .map(x => x.item) } // ─── Component ──────────────────────────────────────────────────────────────── export function CommandPalette() { const [open, setOpen] = useState(false) const [query, setQuery] = useState("") const [activeIdx, setActiveIdx] = useState(0) const inputRef = useRef(null) const listRef = useRef(null) const router = useRouter() const pathname = usePathname() const results = useMemo(() => filterItems(query), [query]) // ── Keyboard shortcut to open ───────────────────────────────────────────── useEffect(() => { function onKey(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault() setOpen(prev => { if (!prev) { setQuery(""); setActiveIdx(0) } return !prev }) } if (e.key === "Escape") setOpen(false) } window.addEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey) }, []) // ── Focus input on open ─────────────────────────────────────────────────── useEffect(() => { if (open) setTimeout(() => inputRef.current?.focus(), 10) }, [open]) // ── Close on route change ───────────────────────────────────────────────── useEffect(() => { queueMicrotask(() => setOpen(false)) }, [pathname]) // ── Arrow key + Enter navigation ────────────────────────────────────────── function onInputKey(e: React.KeyboardEvent) { if (e.key === "ArrowDown") { e.preventDefault() setActiveIdx(i => Math.min(i + 1, results.length - 1)) } else if (e.key === "ArrowUp") { e.preventDefault() setActiveIdx(i => Math.max(i - 1, 0)) } else if (e.key === "Enter") { e.preventDefault() const item = results[activeIdx] if (item) navigate(item) } } function navigate(item: CommandItem) { router.push(item.url) setOpen(false) } // ── Scroll active item into view ────────────────────────────────────────── useEffect(() => { const el = listRef.current?.querySelector(`[data-idx="${activeIdx}"]`) as HTMLElement | null el?.scrollIntoView({ block: "nearest" }) }, [activeIdx]) // ── Reset active idx when query changes ─────────────────────────────────── useEffect(() => { queueMicrotask(() => setActiveIdx(0)) }, [query]) // ── Grouped results for display ─────────────────────────────────────────── const grouped = useMemo(() => { const map = new Map() for (const item of results) { if (!map.has(item.group)) map.set(item.group, []) map.get(item.group)!.push(item) } return Array.from(map.entries()) }, [results]) // ── Running index across groups ─────────────────────────────────────────── let runningIdx = 0 if (!open) return null return (
setOpen(false)} > {/* Backdrop */}
{/* Dialog */}
e.stopPropagation()} > {/* Search input row */}
setQuery(e.target.value)} onKeyDown={onInputKey} placeholder="Поиск страниц и функций…" className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none" /> {query && ( )} ESC
{/* Results */}
{results.length === 0 ? (
Ничего не найдено по “{query}”
) : grouped.map(([group, items]) => (
{/* Group header */}
{group}
{items.map(item => { const idx = runningIdx++ const isActive = idx === activeIdx return ( ) })}
))}
{/* Footer */}
↑↓ навигация открыть Esc закрыть Ctrl {" + "} K
) }