Files
EvoFirewall/apps/web/src/components/agents/agent-facts-panel.tsx
T
Denozordec 3dc8e6d5e2 refactor(web): общие форматтеры, единая навигация, RBAC-гейтинг и чистка мёртвого кода
- lib/format.ts: ru-RU форматтеры дат/чисел в одном месте — убраны дубли
  из 8 компонентов (agent-card, fleet-grid, blocked-ips/ports, facts,
  lifecycle, host-firewall, lists-columns, system-monitor)
- lib/nav.ts: единый конфиг маршрутов — sidebar, ⌘K-поиск и breadcrumbs
  рендерятся из одного источника (было 3 расходящихся копии)
- lib/permissions.ts + useCan: клиентский RBAC — кнопки создания/подтверждения
  скрываются без fw:*:write/admin (сервер остаётся авторитетным)
- удалён мёртвый код ~2500 строк: stepper, data-grid dnd/virtual/visibility
  варианты, settings-shell
2026-09-20 19:36:37 +07:00

128 lines
4.3 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { Agent, DefaultAction } from '@evofw/shared'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { apiFetch } from '@/lib/api'
import { Field, FieldLabel } from '@evofw/ui/components/field'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evofw/ui/components/select'
import { Separator } from '@evofw/ui/components/separator'
import { formatDateTime } from '@/lib/format'
/**
* Agent facts panel — SA3 RunFacts DNA (editable default_action).
* Preview: https://reui.io/preview/base/solution-agents-3
* · https://reui.io/preview/base/settings-3
*/
type AgentFactsPanelProps = {
agent: Agent
}
function formatWhen(iso?: string | null): string {
if (!iso) return '—'
return formatDateTime(iso)
}
export function AgentFactsPanel({ agent }: AgentFactsPanelProps) {
const qc = useQueryClient()
const defaultAction: DefaultAction =
agent.default_action === 'drop' ? 'drop' : 'accept'
const patch = useMutation({
mutationFn: (default_action: DefaultAction) =>
apiFetch<Agent>(`/api/v1/agents/${agent.id}`, {
method: 'PATCH',
body: JSON.stringify({ default_action }),
}),
onSuccess: () => {
toast.success('Default action обновлён')
void qc.invalidateQueries({ queryKey: ['agents'] })
void qc.invalidateQueries({ queryKey: ['agents', agent.id, 'preview'] })
},
onError: (e: Error) => toast.error(e.message),
})
return (
<Frame dense spacing="sm">
<FrameHeader>
<FrameTitle>Параметры</FrameTitle>
<FrameDescription>Default + identity</FrameDescription>
</FrameHeader>
<FramePanel className="flex flex-col gap-4">
<Field>
<FieldLabel>Если не совпало</FieldLabel>
<Select
value={defaultAction}
onValueChange={(v) => {
if (v === 'accept' || v === 'drop') patch.mutate(v)
}}
disabled={patch.isPending}
items={[
{ value: 'accept', label: 'Accept' },
{ value: 'drop', label: 'Drop' },
]}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="accept">Accept</SelectItem>
<SelectItem value="drop">Drop</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">
Пакет вне deny/allow → {defaultAction === 'drop' ? 'DROP' : 'ACCEPT'}
</p>
</Field>
<Separator />
<dl className="grid gap-2 text-sm">
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Hostname</dt>
<dd className="truncate font-medium">{agent.hostname ?? '—'}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Token</dt>
<dd className="font-mono text-xs">{agent.token_prefix}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Client</dt>
<dd>{agent.client_version ?? '—'}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Last seen IP</dt>
<dd>{agent.last_seen_ip ?? '—'}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Created</dt>
<dd className="text-right text-xs">{formatWhen(agent.created_at)}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Approved</dt>
<dd className="text-right text-xs">
{formatWhen(agent.approved_at)}
</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Generation</dt>
<dd className="tabular-nums">{agent.policy_generation}</dd>
</div>
</dl>
</FramePanel>
</Frame>
)
}