import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { useRef, useState } from 'react' import { ActivityIcon, CircleAlertIcon, ClockIcon, Copy, CopyPlusIcon, CpuIcon, MoreHorizontalIcon, ShieldPlusIcon, TerminalIcon, Trash2, } from 'lucide-react' import { DetailPanel } from '@/components/reui-kit' import { Alert, AlertDescription, AlertTitle, } from '@/components/reui/alert' import { Badge } from '@/components/reui/badge' import { StatusBadge } from '@/components/status-badge' import { AgentPlatformIcon, platformLabel } from '@/components/agents/agent-platform-icon' import { AgentOnlineDot } from '@/components/agents/agent-online-dot' import { agentTrafficAccepted, agentTrafficDropped, } from '@/components/agents/agent-traffic' import { AgentPolicySetsSortable } from '@/components/agents/agent-policy-sets-sortable' import { AgentPolicyTrace } from '@/components/agents/agent-policy-trace' import { AgentFactsPanel } from '@/components/agents/agent-facts-panel' import { AgentEffectiveCidrs } from '@/components/agents/agent-effective-cidrs' import { AgentBlockedIps } from '@/components/agents/agent-blocked-ips' import { AgentBlockedPorts } from '@/components/agents/agent-blocked-ports' import { AgentHostFirewall } from '@/components/agents/agent-host-firewall' import { AgentPortAcl } from '@/components/agents/agent-port-acl' import { CountedLineTabs } from '@/components/counted-line-tabs' import { ConfirmDialog } from '@/components/confirm-dialog' import { AgentCloneSetsSheet, AgentOverrideSheet, } from '@/components/agents/agent-settings-sheets' import { agentPreviewQueryOptions, agentQueryOptions, } from '@/queries' import { apiFetch } from '@/lib/api' import { formatDateTime, formatRelativeTime } from '@/lib/format' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Button } from '@evofw/ui/components/button' import { Skeleton } from '@evofw/ui/components/skeleton' import { TabsContent } from '@evofw/ui/components/tabs' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@evofw/ui/components/dropdown-menu' /** * Full agent detail body — SA3 DNA for Sheet (and redirect target). * Preview: https://reui.io/preview/base/solution-agents-3 * · https://reui.io/preview/base/stats-12 * · https://reui.io/preview/base/form-7 */ type AgentDetailViewProps = { agentId: string onDelete?: (id: string) => void } export function AgentDetailView({ agentId, onDelete }: AgentDetailViewProps) { const qc = useQueryClient() const { copyToClipboard } = useCopyToClipboard() const agentQ = useQuery(agentQueryOptions(agentId)) const previewQ = useQuery(agentPreviewQueryOptions(agentId)) const installRef = useRef(null) const [overrideOpen, setOverrideOpen] = useState(false) const [cloneOpen, setCloneOpen] = useState(false) const [revokeOpen, setRevokeOpen] = useState(false) const [resetStatsOpen, setResetStatsOpen] = useState(false) const [fwTab, setFwTab] = useState('host') const revoke = useMutation({ mutationFn: () => apiFetch(`/api/v1/agents/${agentId}/revoke`, { method: 'POST' }), onSuccess: () => { toast.success('Агент отозван') void qc.invalidateQueries({ queryKey: ['agents'] }) }, onError: (e: Error) => toast.error(e.message), }) const approve = useMutation({ mutationFn: () => apiFetch(`/api/v1/agents/${agentId}/approve`, { method: 'POST' }), onSuccess: () => { toast.success('Агент одобрен') void qc.invalidateQueries({ queryKey: ['agents'] }) }, onError: (e: Error) => toast.error(e.message), }) const resetStats = useMutation({ mutationFn: () => apiFetch(`/api/v1/agents/${agentId}/stats/reset`, { method: 'POST' }), onSuccess: () => { toast.success('Статистика сброшена') void qc.invalidateQueries({ queryKey: ['agents'] }) void qc.invalidateQueries({ queryKey: ['agents', agentId] }) 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: ['dashboard'] }) }, onError: (e: Error) => toast.error(e.message), }) const a = agentQ.data if (agentQ.isLoading || !a) { return (
) } if (agentQ.isError) { return (
Ошибка загрузки {agentQ.error?.message ?? 'Не удалось загрузить агента'}
) } const headerDesc = [ a.hostname, platformLabel(a.platform), `поколение ${a.policy_generation}`, a.default_action === 'drop' ? 'по умолчанию: блокировать' : 'по умолчанию: пропускать', ] .filter(Boolean) .join(' · ') return ( <>
{a.default_action === 'drop' ? 'Drop' : 'Accept'} {a.status === 'pending' ? ( ) : null} {a.status === 'approved' ? ( ) : null} {a.install_curl ? ( ) : null} } > setOverrideOpen(true)}> IP override setCloneOpen(true)}> Копировать наборы {a.install_curl ? ( { copyToClipboard(a.install_curl!) toast.success('Скопировано') installRef.current?.scrollIntoView({ behavior: 'smooth', }) }} > Install curl ) : null} {onDelete ? ( <> onDelete(agentId)} > Удалить агента ) : null} } /> {a.last_apply_error ? ( Ошибка apply {a.last_apply_error} ) : null} , iconClassName: 'text-warning', label: 'Traffic', description: `↓${agentTrafficDropped(a)} · ↑${agentTrafficAccepted(a)}`, hint: 'накопительно', variant: 'warning', footer: ( ), }, { id: 'kernel', icon: , iconClassName: 'text-info', label: 'Kernel', description: a.last_apply_kernel_method ?? '—', }, { id: 'apply', icon: , iconClassName: 'text-primary', label: 'Last apply', description: formatDateTime(a.last_apply_at), hint: a.last_apply_at ? formatRelativeTime(a.last_apply_at) : undefined, }, ]} />
{a.platform === 'linux' ? (
) : ( )}
{ setRevokeOpen(false) revoke.mutate() }} disabled={revoke.isPending} /> { setResetStatsOpen(false) resetStats.mutate() }} disabled={resetStats.isPending} /> ) }