From 28f1f35f16f98c702eb8c896d2df7c943fed693c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 25 Sep 2026 01:26:14 +0700 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D1=86=D0=B5=D0=BB=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9,=20=D1=80=D1=83=D1=81=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BE=D0=B1=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=BA=D0=B8,=20RBAC-=D0=B3=D0=B5=D0=B9=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8,=20=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20=D0=BC=D1=91?= =?UTF-8?q?=D1=80=D1=82=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/components/agents/agent-card.tsx | 73 ++++++++--- .../components/agents/agent-detail-view.tsx | 26 ++-- .../agents/agent-fleet-data-grid.tsx | 44 ++++--- .../agents/agent-lifecycle-timeline.tsx | 122 ------------------ apps/web/src/components/app-sidebar.tsx | 11 +- apps/web/src/components/form-field.tsx | 35 ----- .../src/components/reui-kit/resource-page.tsx | 28 +++- apps/web/src/components/status-badge.tsx | 16 +-- apps/web/src/lib/nav.ts | 8 ++ apps/web/src/routes/_auth/agents/index.tsx | 16 +-- apps/web/src/routes/_auth/lists/$id.tsx | 23 ++++ apps/web/src/routes/_auth/rules/$setId.tsx | 31 +++++ 12 files changed, 205 insertions(+), 228 deletions(-) delete mode 100644 apps/web/src/components/agents/agent-lifecycle-timeline.tsx delete mode 100644 apps/web/src/components/form-field.tsx diff --git a/apps/web/src/components/agents/agent-card.tsx b/apps/web/src/components/agents/agent-card.tsx index 0cf5046..c01219e 100644 --- a/apps/web/src/components/agents/agent-card.tsx +++ b/apps/web/src/components/agents/agent-card.tsx @@ -1,9 +1,14 @@ import type { Agent } from '@evofw/shared' -import { Trash2 } from 'lucide-react' +import { + MoreVerticalIcon, + PanelRight, + Trash2, +} from 'lucide-react' import { AgentPlatformIcon, platformLabel, } from '@/components/agents/agent-platform-icon' +import { AgentOnlineDot } from '@/components/agents/agent-online-dot' import { agentHasTrafficSample, agentTrafficAccepted, @@ -13,6 +18,13 @@ import { StatusBadge } from '@/components/status-badge' import { Badge } from '@/components/reui/badge' import { Frame, FramePanel } from '@/components/reui/frame' import { Button } from '@evofw/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@evofw/ui/components/dropdown-menu' import { Item, ItemContent, @@ -21,7 +33,7 @@ import { } from '@evofw/ui/components/item' import { Separator } from '@evofw/ui/components/separator' import { cn } from '@evofw/ui/lib/utils' -import { formatPackets, formatShortDateTime } from '@/lib/format' +import { formatPackets, formatRelativeTime } from '@/lib/format' /** * Agent catalog card — hybrid card-3 header + stats strip + stats-12 values. @@ -48,20 +60,22 @@ export function AgentCard({ dropped === '—' && accepted === '—' ? '—' : `↓${dropped} · ↑${accepted}` - const seen = formatShortDateTime(agent.last_seen_at ?? agent.last_apply_at) + const seen = formatRelativeTime( + agent.last_seen_at ?? agent.last_apply_at, + ) const defaultAction = - agent.default_action === 'drop' ? 'Drop' : 'Accept' + agent.default_action === 'drop' ? 'Блокировать' : 'Пропускать' const subtitle = [ agent.hostname, platformLabel(agent.platform), - `gen ${agent.policy_generation}`, + `поколение ${agent.policy_generation}`, ] .filter(Boolean) .join(' · ') const stats = [ { - label: 'Traffic', + label: 'Трафик', value: traffic, valueClass: traffic === '—' @@ -79,7 +93,7 @@ export function AgentCard({ ), }, { - label: 'Seen', + label: 'Активность', value: seen, valueClass: 'text-muted-foreground', valueNode: seen, @@ -91,6 +105,7 @@ export function AgentCard({ - + + + } + > + + + + onSelect(agent.id)}> + + Открыть + + + onDelete(agent.id)} + > + + Удалить + + + ) } diff --git a/apps/web/src/components/agents/agent-detail-view.tsx b/apps/web/src/components/agents/agent-detail-view.tsx index c2c07f9..c88f76f 100644 --- a/apps/web/src/components/agents/agent-detail-view.tsx +++ b/apps/web/src/components/agents/agent-detail-view.tsx @@ -113,7 +113,7 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { void qc.invalidateQueries({ queryKey: ['agents', agentId, 'stats'] }) void qc.invalidateQueries({ queryKey: ['agents', agentId, 'blocked-ips'] }) void qc.invalidateQueries({ queryKey: ['agents', agentId, 'blocked-ports'] }) - void qc.invalidateQueries({ queryKey: ['stats'] }) + void qc.invalidateQueries({ queryKey: ['stats-recent'] }) void qc.invalidateQueries({ queryKey: ['dashboard'] }) }, onError: (e: Error) => toast.error(e.message), @@ -196,7 +196,7 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { onClick={() => approve.mutate()} disabled={approve.isPending} > - Approve + Утвердить ) : null} {a.status === 'approved' ? ( @@ -215,11 +215,11 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { size="sm" onClick={() => { copyToClipboard(a.install_curl!) - toast.success('Скопировано') + toast.success('Команда установки скопирована') }} > - Install + Установка ) : null} @@ -247,14 +247,14 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { { copyToClipboard(a.install_curl!) - toast.success('Скопировано') + toast.success('Команда установки скопирована') installRef.current?.scrollIntoView({ behavior: 'smooth', }) }} > - Install curl + Команда установки ) : null} {onDelete ? ( @@ -278,7 +278,7 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { {a.last_apply_error ? ( - Ошибка apply + Ошибка применения политики {a.last_apply_error} ) : null} @@ -289,7 +289,7 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { id: 'traffic', icon: , iconClassName: 'text-warning', - label: 'Traffic', + label: 'Трафик', description: `↓${agentTrafficDropped(a)} · ↑${agentTrafficAccepted(a)}`, hint: 'накопительно', variant: 'warning', @@ -309,14 +309,14 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { id: 'kernel', icon: , iconClassName: 'text-info', - label: 'Kernel', + label: 'Ядро', description: a.last_apply_kernel_method ?? '—', }, { id: 'apply', icon: , iconClassName: 'text-primary', - label: 'Last apply', + label: 'Последнее применение', description: formatDateTime(a.last_apply_at), hint: a.last_apply_at ? formatRelativeTime(a.last_apply_at) @@ -352,9 +352,9 @@ export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { value={fwTab} onValueChange={setFwTab} tabs={[ - { id: 'host', label: 'Host firewall' }, - { id: 'acl', label: 'Port ACL' }, - { id: 'hits', label: 'Blocked' }, + { id: 'host', label: 'Хост-фаервол' }, + { id: 'acl', label: 'Правила портов' }, + { id: 'hits', label: 'Заблокированное' }, ]} > diff --git a/apps/web/src/components/agents/agent-fleet-data-grid.tsx b/apps/web/src/components/agents/agent-fleet-data-grid.tsx index 1788097..02f8fec 100644 --- a/apps/web/src/components/agents/agent-fleet-data-grid.tsx +++ b/apps/web/src/components/agents/agent-fleet-data-grid.tsx @@ -151,12 +151,12 @@ export function AgentFleetDataGrid({ }, { id: 'default_action', - size: 100, - minSize: 90, - maxSize: 120, + size: 130, + minSize: 120, + maxSize: 150, accessorFn: (row) => row.default_action, header: ({ column }) => ( - + ), cell: ({ row }) => { const drop = row.original.default_action === 'drop' @@ -166,19 +166,19 @@ export function AgentFleetDataGrid({ size="sm" radius="full" > - {drop ? 'Drop' : 'Accept'} + {drop ? 'Блокировать' : 'Пропускать'} ) }, }, { id: 'apply', - size: 100, - minSize: 90, - maxSize: 120, + size: 110, + minSize: 100, + maxSize: 130, accessorFn: (row) => row.last_apply_status ?? '', header: ({ column }) => ( - + ), cell: ({ row }) => { const a = row.original @@ -191,7 +191,7 @@ export function AgentFleetDataGrid({ } > - error + Ошибка @@ -203,9 +203,10 @@ export function AgentFleetDataGrid({ if (!a.last_apply_status && !a.last_apply_at) { return — } + const status = a.last_apply_status ?? 'ok' return ( - {a.last_apply_status ?? 'ok'} + {status === 'applied' ? 'Применено' : status} ) }, @@ -218,7 +219,7 @@ export function AgentFleetDataGrid({ accessorFn: (row) => agentTrafficDropped(row) + agentTrafficAccepted(row), header: ({ column }) => ( - + ), cell: ({ row }) => { const a = row.original @@ -265,15 +266,18 @@ export function AgentFleetDataGrid({ }, { id: 'gen', - size: 70, - minSize: 60, - maxSize: 90, + size: 110, + minSize: 100, + maxSize: 130, accessorFn: (row) => row.policy_generation, header: ({ column }) => ( - + ), cell: ({ row }) => ( - + {row.original.policy_generation} ), @@ -294,7 +298,7 @@ export function AgentFleetDataGrid({ + + + + ) + } + if (!detail) { return ( diff --git a/apps/web/src/routes/_auth/rules/$setId.tsx b/apps/web/src/routes/_auth/rules/$setId.tsx index d5c4d85..7a92083 100644 --- a/apps/web/src/routes/_auth/rules/$setId.tsx +++ b/apps/web/src/routes/_auth/rules/$setId.tsx @@ -2,6 +2,7 @@ import { createFileRoute, Link } from '@tanstack/react-router' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { + CircleAlertIcon, ListIcon, ShieldIcon, UsersIcon, @@ -9,6 +10,11 @@ import { import { useMemo, useState } from 'react' import type { ColumnDef, RowSelectionState } from '@tanstack/react-table' import { PageShell, DetailPanel } from '@/components/reui-kit' +import { + Alert, + AlertDescription, + AlertTitle, +} from '@/components/reui/alert' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridPrimaryCell } from '@/components/data-grid-cell' import { StatusBadge } from '@/components/status-badge' @@ -271,6 +277,31 @@ function PolicySetDetailPage() { ) } + if (setQ.isError) { + return ( + + + + Ошибка загрузки + + + {setQ.error?.message ?? 'Не удалось загрузить набор правил'} + + + + + + ) + } + if (!setQ.data) { return (