Тема в NavUser вместо ModeToggle в header; KpiStatGrid/QuickActionGrid по эталону EvoBGP; обновлены rules и contract. Co-authored-by: Cursor <[email protected]>
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
import { Link, useMatches, useRouterState } from '@tanstack/react-router'
|
|
import { useMemo } from 'react'
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
BreadcrumbList,
|
|
BreadcrumbPage,
|
|
BreadcrumbSeparator,
|
|
} from '@evofw/ui/components/breadcrumb'
|
|
import { Separator } from '@evofw/ui/components/separator'
|
|
import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover'
|
|
import { AppsMenu } from '@/components/layout/apps-menu'
|
|
import { SidebarTrigger } from '@evofw/ui/components/sidebar'
|
|
|
|
export interface RouteBreadcrumbLoaderData {
|
|
breadcrumb?: string
|
|
}
|
|
|
|
const routeTitles: Record<string, string> = {
|
|
'/': 'Панель управления',
|
|
'/agents': 'Агенты',
|
|
'/lists': 'Списки',
|
|
'/rules': 'Наборы правил',
|
|
'/stats': 'Статистика',
|
|
'/settings': 'Настройки',
|
|
}
|
|
|
|
function getBreadcrumbs(
|
|
pathname: string,
|
|
dynamicLabels: Record<string, string>,
|
|
) {
|
|
if (pathname === '/') {
|
|
return [{ label: 'Панель управления', href: '/' }]
|
|
}
|
|
|
|
if (pathname.match(/^\/agents\/[^/]+$/)) {
|
|
return [
|
|
{ label: 'Агенты', href: '/agents' },
|
|
{ label: dynamicLabels[pathname] ?? 'Агент', href: pathname },
|
|
]
|
|
}
|
|
|
|
if (pathname.match(/^\/rules\/[^/]+$/)) {
|
|
return [
|
|
{ label: 'Наборы правил', href: '/rules' },
|
|
{ label: dynamicLabels[pathname] ?? 'Набор', href: pathname },
|
|
]
|
|
}
|
|
|
|
const title = routeTitles[pathname]
|
|
if (title) {
|
|
return [{ label: title, href: pathname }]
|
|
}
|
|
|
|
return [{ label: 'Панель управления', href: '/' }]
|
|
}
|
|
|
|
function useDynamicBreadcrumbLabels() {
|
|
const matches = useMatches()
|
|
return useMemo(() => {
|
|
const labels: Record<string, string> = {}
|
|
for (const match of matches) {
|
|
const data = match.loaderData as RouteBreadcrumbLoaderData | undefined
|
|
if (data?.breadcrumb && match.pathname) {
|
|
labels[match.pathname] = data.breadcrumb
|
|
}
|
|
}
|
|
return labels
|
|
}, [matches])
|
|
}
|
|
|
|
/** Header chrome — AppsMenu + SystemMonitor; theme in NavUser. */
|
|
export function SiteHeader() {
|
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
const dynamicLabels = useDynamicBreadcrumbLabels()
|
|
const crumbs = getBreadcrumbs(pathname, dynamicLabels)
|
|
|
|
return (
|
|
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4 md:px-6">
|
|
<SidebarTrigger className="-ml-1" />
|
|
<Separator
|
|
orientation="vertical"
|
|
className="mr-2 data-vertical:h-4 data-vertical:self-center"
|
|
/>
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
{crumbs.map((crumb, index) => {
|
|
const isLast = index === crumbs.length - 1
|
|
return (
|
|
<span key={crumb.href} className="contents">
|
|
{index > 0 && (
|
|
<BreadcrumbSeparator className="hidden md:block" />
|
|
)}
|
|
<BreadcrumbItem
|
|
className={
|
|
index === 0 && !isLast ? 'hidden md:block' : undefined
|
|
}
|
|
>
|
|
{isLast ? (
|
|
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
|
) : (
|
|
<BreadcrumbLink render={<Link to={crumb.href} />}>
|
|
{crumb.label}
|
|
</BreadcrumbLink>
|
|
)}
|
|
</BreadcrumbItem>
|
|
</span>
|
|
)
|
|
})}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<AppsMenu />
|
|
<SystemMonitorPopover />
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|