- 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
128 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|