diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index eb2e1ee..06cc5f4 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -22,7 +22,7 @@ import { } from '@evofw/ui/components/sidebar' const overviewNav = [ - { to: '/', label: 'Дашборд', icon: LayoutDashboardIcon, exact: true }, + { to: '/', label: 'Панель управления', icon: LayoutDashboardIcon, exact: true }, ] as const const opsNav = [ diff --git a/apps/web/src/components/form-sheet.tsx b/apps/web/src/components/form-sheet.tsx index c9af3f4..6f7c33a 100644 --- a/apps/web/src/components/form-sheet.tsx +++ b/apps/web/src/components/form-sheet.tsx @@ -24,6 +24,11 @@ interface FormSheetProps { contentClassName?: string } +/** + * Side sheet form shell — shadcn sheet-demo + ReUI sheet-8 layout. + * Preview: https://reui.io/preview/base/sheet-8 + * Docs: https://ui.shadcn.com/docs/components/base/sheet + */ export function FormSheet({ open, onOpenChange, @@ -41,11 +46,11 @@ export function FormSheet({ return ( {title} - {description && {description}} + {description ? {description} : null}
({ >
{children}
{footer ? ( - {footer} + + {footer} + ) : null}
diff --git a/apps/web/src/components/layout/search-menu.tsx b/apps/web/src/components/layout/search-menu.tsx index b188dbe..c8be00f 100644 --- a/apps/web/src/components/layout/search-menu.tsx +++ b/apps/web/src/components/layout/search-menu.tsx @@ -29,7 +29,7 @@ import { const NAV_ITEMS = [ { to: '/', - label: 'Дашборд', + label: 'Панель управления', keywords: ['dashboard', 'панель', 'обзор'], icon: LayoutDashboardIcon, }, diff --git a/apps/web/src/components/layout/site-header.tsx b/apps/web/src/components/layout/site-header.tsx index 1ed7ebb..ab221f5 100644 --- a/apps/web/src/components/layout/site-header.tsx +++ b/apps/web/src/components/layout/site-header.tsx @@ -19,7 +19,7 @@ export interface RouteBreadcrumbLoaderData { } const routeTitles: Record = { - '/': 'Дашборд', + '/': 'Панель управления', '/agents': 'Агенты', '/lists': 'Списки IP', '/rules': 'Правила', @@ -32,7 +32,7 @@ function getBreadcrumbs( dynamicLabels: Record, ) { if (pathname === '/') { - return [{ label: 'Дашборд', href: '/' }] + return [{ label: 'Панель управления', href: '/' }] } if (pathname.match(/^\/agents\/[^/]+$/)) { @@ -47,7 +47,7 @@ function getBreadcrumbs( return [{ label: title, href: pathname }] } - return [{ label: 'Дашборд', href: '/' }] + return [{ label: 'Панель управления', href: '/' }] } function useDynamicBreadcrumbLabels() { @@ -73,7 +73,10 @@ export function SiteHeader() { return (
- + {crumbs.map((crumb, index) => { @@ -84,7 +87,9 @@ export function SiteHeader() { )} {isLast ? ( {crumb.label} diff --git a/apps/web/src/components/layout/system-monitor-popover.tsx b/apps/web/src/components/layout/system-monitor-popover.tsx index d49f1e5..6643c7e 100644 --- a/apps/web/src/components/layout/system-monitor-popover.tsx +++ b/apps/web/src/components/layout/system-monitor-popover.tsx @@ -7,7 +7,6 @@ import { cn } from '@evofw/ui/lib/utils' import { Item, ItemMedia } from '@evofw/ui/components/item' import { Popover, PopoverContent, PopoverTrigger } from '@evofw/ui/components/popover' import { Progress } from '@evofw/ui/components/progress' -import { Button } from '@evofw/ui/components/button' import { agentsQueryOptions, dashboardQueryOptions, @@ -53,11 +52,18 @@ function MetricCell({ metric }: { metric: MonitorMetric }) { {metric.icon} - {metric.label} + + {metric.label} + - + {metric.value} - {metric.unit} + + {metric.unit} + a.status === 'approved').length const online = d?.agents_online ?? agents.filter((a) => a.last_seen_at).length - const pending = d?.agents_pending ?? agents.filter((a) => a.status === 'pending').length + const pending = + d?.agents_pending ?? agents.filter((a) => a.status === 'pending').length const onlinePct = approved > 0 ? Math.round((online / approved) * 100) : 0 const listsCount = listsQ.data?.items?.length ?? d?.lists_total ?? 0 const rulesCount = rulesQ.data?.items?.length ?? 0 @@ -105,7 +112,12 @@ export function SystemMonitorPopover() { unit: '%', percent: onlinePct, icon: , - tone: onlinePct >= 80 ? 'success' : onlinePct >= 40 ? 'warning' : 'destructive', + tone: + onlinePct >= 80 + ? 'success' + : onlinePct >= 40 + ? 'warning' + : 'destructive', alert: pending > 0 || (approved > 0 && onlinePct < 50), }, { @@ -132,42 +144,86 @@ export function SystemMonitorPopover() { [apiOk, onlinePct, pending, approved, listsCount, rulesCount], ) - const hasAlert = metrics.some((m) => m.alert) + const spiking = metrics.some((m) => m.alert) return ( } > - - {hasAlert ? ( - + - ) : null} + {spiking ? ( + + ) : null} + + + Система + + + {spiking ? 'Внимание' : 'Норма'} + - -
-
Система
-
- Агенты и политики EvoFirewall -
+ + +
+ + Монитор EvoFirewall + + + {new Date().toLocaleTimeString('ru-RU')} +
-
- {metrics.map((m) => ( - +
+ {metrics.map((metric, i) => ( +
= 2 && 'border-border border-t', + )} + > + +
))}
+
+ Pending:{' '} + + {pending} + + {' · '} + Online:{' '} + + {online}/{approved || '—'} + +
) diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx index 2ef6339..8a14c74 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -115,7 +115,11 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) { if (card.to) { return ( - + diff --git a/apps/web/src/components/reui-kit/quick-action-grid.tsx b/apps/web/src/components/reui-kit/quick-action-grid.tsx index 8dbd977..bb77a82 100644 --- a/apps/web/src/components/reui-kit/quick-action-grid.tsx +++ b/apps/web/src/components/reui-kit/quick-action-grid.tsx @@ -99,7 +99,7 @@ export function QuickActionGrid({ > diff --git a/apps/web/src/routes/_auth/index.tsx b/apps/web/src/routes/_auth/index.tsx index 99994e5..1cd8f23 100644 --- a/apps/web/src/routes/_auth/index.tsx +++ b/apps/web/src/routes/_auth/index.tsx @@ -1,7 +1,6 @@ import { createFileRoute, Link } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { useMemo, useState } from 'react' -import type { ColumnDef } from '@tanstack/react-table' +import { useMemo } from 'react' import { ServerIcon, BanIcon, @@ -23,32 +22,46 @@ import { FrameTitle, } from '@/components/reui/frame' import { Badge } from '@/components/reui/badge' +import { + Item, + ItemContent, + ItemGroup, + ItemTitle, +} from '@evofw/ui/components/item' import { dashboardQueryOptions, agentsQueryOptions, recentStatsQueryOptions, } from '@/queries' import type { Agent } from '@evofw/shared' -import { DataGrid } from '@/components/reui/data-grid/data-grid' -import { DataGridTable } from '@/components/reui/data-grid/data-grid-table' -import { - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table' export const Route = createFileRoute('/_auth/')({ component: DashboardPage, }) +const EMPTY_AGENTS: Agent[] = [] + function DashboardPage() { const dash = useQuery(dashboardQueryOptions()) const agents = useQuery(agentsQueryOptions()) const stats = useQuery(recentStatsQueryOptions()) const d = dash.data - const items = agents.data?.items ?? [] - const pending = items.filter((a) => a.status === 'pending') - const applyErrors = items.filter((a) => a.last_apply_error) + const items = agents.data?.items ?? EMPTY_AGENTS + + const pending = useMemo( + () => items.filter((a) => a.status === 'pending'), + [items], + ) + const applyErrors = useMemo( + () => items.filter((a) => a.last_apply_error), + [items], + ) + const previewAgents = useMemo(() => items.slice(0, 8), [items]) + const samples = useMemo( + () => (stats.data?.items ?? []).slice(0, 10), + [stats.data?.items], + ) const kpiCards: KpiStatCard[] = useMemo( () => [ @@ -92,93 +105,40 @@ function DashboardPage() { [d], ) - const quickActions: QuickActionItem[] = [ - { - id: 'agents', - title: 'Агенты', - description: 'Enroll и approve', - to: '/agents', - icon: , - iconClassName: 'text-info', - }, - { - id: 'lists', - title: 'Списки', - description: 'Blocklists / sources', - to: '/lists', - icon: , - iconClassName: 'text-primary', - }, - { - id: 'rules', - title: 'Правила', - description: 'Allow / deny policy', - to: '/rules', - icon: , - iconClassName: 'text-warning', - }, - ] - - const agentColumns: ColumnDef[] = useMemo( + const quickActions: QuickActionItem[] = useMemo( () => [ { - accessorKey: 'name', - header: 'Имя', - cell: ({ row }) => ( - - {row.original.name} - - ), + id: 'agents', + title: 'Агенты', + description: 'Enroll и approve', + to: '/agents', + icon: , + iconClassName: 'text-info', }, { - accessorKey: 'status', - header: 'Статус', - cell: ({ row }) => ( - - {row.original.status} - - ), + id: 'lists', + title: 'Списки', + description: 'Blocklists / sources', + to: '/lists', + icon: , + iconClassName: 'text-primary', }, - { accessorKey: 'policy_mode', header: 'Режим' }, { - accessorKey: 'last_apply_packets_dropped', - header: 'Dropped', - cell: ({ row }) => ( - - {row.original.last_apply_packets_dropped ?? 0} - - ), + id: 'rules', + title: 'Правила', + description: 'Allow / deny policy', + to: '/rules', + icon: , + iconClassName: 'text-warning', }, ], [], ) - const agentsTable = useReactTable({ - data: items.slice(0, 8), - columns: agentColumns, - getCoreRowModel: getCoreRowModel(), - getRowId: (r) => r.id, - }) - - const samples = (stats.data?.items ?? []).slice(0, 10) - return ( Агенты - - - - + + {previewAgents.length === 0 ? ( +

Нет агентов

+ ) : ( + + {previewAgents.map((a) => ( + + + + + {a.name} + + +
+ + {a.status} + + + ↓{a.last_apply_packets_dropped ?? 0} + +
+
+
+ ))} +
+ )}
@@ -252,7 +247,10 @@ function DashboardPage() { {applyErrors.map((a) => (
{a.name} - — {a.last_apply_error} + + {' '} + — {a.last_apply_error} +
))}
diff --git a/apps/web/src/routes/_auth/lists.tsx b/apps/web/src/routes/_auth/lists.tsx index 0b9b64a..9debc0b 100644 --- a/apps/web/src/routes/_auth/lists.tsx +++ b/apps/web/src/routes/_auth/lists.tsx @@ -8,8 +8,8 @@ import { PageHeader, PageShell, ResourcePage } from '@/components/reui-kit' import { listsQueryOptions } from '@/queries' import { apiFetch } from '@/lib/api' import { Button } from '@evofw/ui/components/button' +import { Field, FieldLabel } from '@evofw/ui/components/field' import { Input } from '@evofw/ui/components/input' -import { Label } from '@evofw/ui/components/label' import { Textarea } from '@evofw/ui/components/textarea' import { Select, @@ -205,18 +205,22 @@ function ListsPage() { /> - - + + Новый список Источник префиксов для правил -
-
- - setName(e.target.value)} /> -
-
- +
+ + Имя + setName(e.target.value)} + /> + + + Тип -
-
- +