feat: реализовать EvoFirewall V1 control plane
API, UI, Linux/MikroTik agents, IP lists, политики, stats, CI и интеграция с auth-portal/EvoBGP. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { Link, useRouterState } from '@tanstack/react-router'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Server,
|
||||
List,
|
||||
Shield,
|
||||
BarChart3,
|
||||
Settings,
|
||||
LogOut,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from '@evofw/ui/components/sidebar'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Separator } from '@evofw/ui/components/separator'
|
||||
import { logout, CURRENT_APP_ID } from '@/lib/auth'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const NAV = [
|
||||
{ to: '/', label: 'Дашборд', icon: LayoutDashboard },
|
||||
{ to: '/agents', label: 'Агенты', icon: Server },
|
||||
{ to: '/lists', label: 'Списки IP', icon: List },
|
||||
{ to: '/rules', label: 'Правила', icon: Shield },
|
||||
{ to: '/stats', label: 'Статистика', icon: BarChart3 },
|
||||
{ to: '/settings', label: 'Настройки', icon: Settings },
|
||||
] as const
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': '240px',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="border-b px-3 py-3">
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<Shield className="size-5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">EvoFirewall</span>
|
||||
<span className="text-muted-foreground text-[10px] uppercase">
|
||||
{CURRENT_APP_ID}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{NAV.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active =
|
||||
item.to === '/'
|
||||
? pathname === '/'
|
||||
: pathname.startsWith(item.to)
|
||||
return (
|
||||
<SidebarMenuItem key={item.to}>
|
||||
<SidebarMenuButton
|
||||
isActive={active}
|
||||
render={<Link to={item.to} />}
|
||||
>
|
||||
<Icon />
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="border-t p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={() => logout()}
|
||||
>
|
||||
<LogOut className="size-4" />
|
||||
Выйти
|
||||
</Button>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-12 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span className="text-muted-foreground text-sm">Control plane</span>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
|
||||
{children}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user