feat: реализовать EvoFirewall V1 control plane
Build and Push EvoFirewall Docker Image / build-and-push (push) Failing after 25s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

API, UI, Linux/MikroTik agents, IP lists, политики, stats, CI и интеграция с auth-portal/EvoBGP.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-20 19:50:54 +07:00
co-authored by Cursor
parent d71b45d86f
commit ebadf70e2b
107 changed files with 15196 additions and 99 deletions
@@ -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>
)
}