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>
)
}
+104
View File
@@ -0,0 +1,104 @@
import type { ReactNode } from 'react'
import { Link } from '@tanstack/react-router'
import { Frame } from '@/components/reui/frame'
import { cn } from '@evofw/ui/lib/utils'
export type KpiItem = {
id: string
label: string
value: string | number
hint?: string
to?: string
}
/** KPI grid — preview: https://reui.io/preview/base/stats-12 */
export function KpiStatGrid({
items,
className,
}: {
items: KpiItem[]
className?: string
}) {
return (
<div className={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-4', className)}>
{items.map((item) => {
const inner = (
<Frame dense className="h-full transition-colors hover:bg-muted/40">
<div className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
{item.label}
</div>
<div className="mt-1 text-2xl font-semibold tabular-nums">{item.value}</div>
{item.hint ? (
<div className="text-muted-foreground mt-1 text-xs">{item.hint}</div>
) : null}
</Frame>
)
return item.to ? (
<Link key={item.id} to={item.to} className="block no-underline">
{inner}
</Link>
) : (
<div key={item.id}>{inner}</div>
)
})}
</div>
)
}
export function PageShell({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<div className={cn('flex flex-col gap-4 md:gap-6', className)}>{children}</div>
)
}
export function PageHeader({
title,
description,
actions,
}: {
title: string
description?: string
actions?: ReactNode
}) {
return (
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex flex-col gap-px">
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
{description ? (
<p className="text-muted-foreground text-sm">{description}</p>
) : null}
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{actions}
</div>
) : null}
</div>
)
}
export function EmptyState({
title,
description,
action,
}: {
title: string
description?: string
action?: ReactNode
}) {
return (
<Frame className="flex flex-col items-center justify-center gap-2 py-12 text-center">
<div className="font-medium">{title}</div>
{description ? (
<p className="text-muted-foreground max-w-md text-sm">{description}</p>
) : null}
{action}
</Frame>
)
}
+59
View File
@@ -0,0 +1,59 @@
import type { ReactNode } from 'react'
import { cn } from '@evofw/ui/lib/utils'
/** Minimal Frame surface (ReUI Frame contract) — preview: https://reui.io/docs/components/base/frame */
export function Frame({
children,
className,
dense,
}: {
children: ReactNode
className?: string
dense?: boolean
}) {
return (
<div
className={cn(
'bg-card text-card-foreground rounded-xl border shadow-xs',
dense ? 'p-3' : 'p-4 md:p-5',
className,
)}
>
{children}
</div>
)
}
export function FrameHeader({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<div className={cn('mb-3 flex flex-wrap items-start justify-between gap-2', className)}>
{children}
</div>
)
}
export function FrameTitle({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return <h2 className={cn('text-base font-semibold tracking-tight', className)}>{children}</h2>
}
export function FrameDescription({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return <p className={cn('text-muted-foreground text-sm', className)}>{children}</p>
}