import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { getCoreRowModel, useReactTable, type ColumnDef, } from '@tanstack/react-table' import { ShieldIcon } from 'lucide-react' import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle, } from '@/components/reui/frame' import { DataGrid } from '@/components/reui/data-grid/data-grid' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridTable } from '@/components/reui/data-grid/data-grid-table' import { Badge } from '@/components/reui/badge' import { EmptyState } from '@/components/empty-state' import { CountedLineTabs } from '@/components/counted-line-tabs' import { agentHostFirewallQueryOptions, type HostFwRuleDto, type HostListenerDto, } from '@/queries' import { Skeleton } from '@evofw/ui/components/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@evofw/ui/components/select' import { TabsContent } from '@evofw/ui/components/tabs' import { formatDateTime } from '@/lib/format' /** * Observed host firewall + listeners (Linux). * Preview: https://reui.io/preview/base/data-grid-filtering-2 * · https://reui.io/preview/base/empty-state-12 */ type AgentHostFirewallProps = { agentId: string } export function AgentHostFirewall({ agentId }: AgentHostFirewallProps) { const q = useQuery(agentHostFirewallQueryOptions(agentId)) const [tab, setTab] = useState('rules') const [ownership, setOwnership] = useState<'all' | 'evofw' | 'foreign'>('all') const [backend, setBackend] = useState('all') const rules = useMemo(() => { let items = q.data?.rules ?? [] if (ownership !== 'all') { items = items.filter((r) => r.ownership === ownership) } if (backend !== 'all') { items = items.filter((r) => r.backend === backend) } return items }, [q.data?.rules, ownership, backend]) const listeners = q.data?.listeners ?? [] const backends = useMemo(() => { const s = new Set((q.data?.rules ?? []).map((r) => r.backend)) return Array.from(s).sort() }, [q.data?.rules]) const ruleCols = useMemo[]>( () => [ { id: 'ownership', accessorKey: 'ownership', header: ({ column }) => ( ), cell: ({ row }) => row.original.ownership === 'evofw' ? ( EvoFW ) : ( foreign ), meta: { headerTitle: 'Owner' }, }, { accessorKey: 'backend', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.backend} ), }, { id: 'chain', accessorFn: (r) => r.chain || r.table || '—', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.chain || row.original.table || '—'} ), }, { accessorKey: 'action', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.action || '—'} ), }, { id: 'ports', accessorFn: (r) => [r.protocol, r.dport].filter(Boolean).join('/') || '—', header: ({ column }) => ( ), cell: ({ row }) => ( {[row.original.protocol, row.original.dport] .filter(Boolean) .join('/') || '—'} ), }, { accessorKey: 'saddr', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.saddr || '—'} ), }, { accessorKey: 'raw', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.raw} ), }, ], [], ) const listenerCols = useMemo[]>( () => [ { accessorKey: 'protocol', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.protocol} ), }, { accessorKey: 'port', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.port} ), }, { accessorKey: 'address', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.address} ), }, { accessorKey: 'process', header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.process || '—'} ), }, ], [], ) const rulesTable = useReactTable({ data: rules, columns: ruleCols, getCoreRowModel: getCoreRowModel(), getRowId: (r, i) => `${r.backend}-${r.chain}-${i}-${r.raw.slice(0, 40)}`, }) const listenersTable = useReactTable({ data: listeners, columns: listenerCols, getCoreRowModel: getCoreRowModel(), getRowId: (r, i) => `${r.protocol}-${r.address}-${r.port}-${i}`, }) return ( Host firewall Снимок nft/iptables/ufw/firewalld + listeners. EvoFW vs foreign. {q.data?.collected_at ? ` Обновлено: ${formatDateTime(q.data.collected_at)}` : ' Пока нет снимка — дождитесь sync агента.'}
{q.isLoading ? (
) : rules.length === 0 ? ( ) : ( )}
{q.isLoading ? ( ) : listeners.length === 0 ? ( ) : ( )}
) }