Files
EvoFirewall/apps/web/src/components/agents/agent-blocked-ports.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

156 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
getCoreRowModel,
useReactTable,
type ColumnDef,
} from '@tanstack/react-table'
import { NetworkIcon } 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 { EmptyState } from '@/components/empty-state'
import { agentBlockedPortsQueryOptions } from '@/queries'
import { Skeleton } from '@evofw/ui/components/skeleton'
import { formatNumber, formatStampDateTime } from '@/lib/format'
/**
* Aggregate destination ports hit by denied sources (Linux nft).
* Preview: https://reui.io/preview/base/data-grid-filtering-2
* · https://reui.io/preview/base/empty-state-12
*/
export type BlockedPortRow = {
port: number
protocol: string
packets: number
last_seen_at: string
}
type AgentBlockedPortsProps = {
agentId: string
}
export function AgentBlockedPorts({ agentId }: AgentBlockedPortsProps) {
const q = useQuery(agentBlockedPortsQueryOptions(agentId))
const columns = useMemo<ColumnDef<BlockedPortRow>[]>(
() => [
{
accessorKey: 'port',
id: 'port',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Port" />
),
cell: ({ row }) => (
<span className="font-mono text-xs tabular-nums">
{row.original.port}
</span>
),
meta: { headerTitle: 'Port' },
},
{
accessorKey: 'protocol',
id: 'protocol',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Proto" />
),
cell: ({ row }) => (
<span className="font-mono text-xs uppercase">
{row.original.protocol}
</span>
),
meta: { headerTitle: 'Proto' },
},
{
accessorKey: 'packets',
id: 'packets',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Packets" />
),
cell: ({ row }) => (
<span className="tabular-nums">
{formatNumber(row.original.packets)}
</span>
),
meta: { headerTitle: 'Packets' },
},
{
accessorKey: 'last_seen_at',
id: 'last_seen_at',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Last seen" />
),
cell: ({ row }) => (
<span className="text-muted-foreground text-xs tabular-nums">
{formatStampDateTime(row.original.last_seen_at)}
</span>
),
meta: { headerTitle: 'Last seen' },
},
],
[],
)
const data = q.data?.items ?? []
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (r) => `${r.protocol}/${r.port}`,
})
return (
<Frame dense spacing="sm">
<FrameHeader>
<FrameTitle>Top ports</FrameTitle>
<FrameDescription>
Destination ports (tcp/udp), в которые слали запросы blocked IP. nft
dynamic set deny_port_hits.
</FrameDescription>
</FrameHeader>
<FramePanel className="p-0">
{q.isLoading ? (
<div className="flex flex-col gap-2 p-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-2/3" />
</div>
) : q.isError ? (
<EmptyState
icon={NetworkIcon}
title="Не удалось загрузить"
description={q.error?.message ?? 'Ошибка API'}
centered={false}
className="py-8"
/>
) : data.length === 0 ? (
<EmptyState
icon={NetworkIcon}
title="Пока нет port hit’ов"
description="Нужен nft + deny_port_hits. После drop с deny появятся tcp/udp dport. Re-run install-ссылки обновляет правила."
centered={false}
className="py-8"
/>
) : (
<DataGrid
table={table}
recordCount={data.length}
tableLayout={{ dense: true }}
>
<DataGridTable />
</DataGrid>
)}
</FramePanel>
</Frame>
)
}