import { useState } from 'react' import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Tag, message } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { FireOutlined } from '@ant-design/icons' import DataTable from '../../components/DataTable' import EmptyState from '../../components/EmptyState' import apiClient, { isEnvelope } from '../../api/client' import type { AddressGroup, AddressObject, FwRule, FwService, FwZone, ServiceGroup, Zone } from './types' import { ZONES_FALLBACK } from './types' interface FormValues { name?: string priority: number enabled: boolean action: FwRule['action'] src_zone: Zone src_kind: 'object' | 'group' | 'cidr' | 'any' src_address_object_id?: number src_address_group_id?: number src_cidr?: string dst_zone: Zone dst_kind: 'object' | 'group' | 'cidr' | 'any' dst_address_object_id?: number dst_address_group_id?: number dst_cidr?: string service_kind: 'object' | 'group' | 'any' service_object_id?: number service_group_id?: number log: boolean comment?: string } const ACTION_COLORS: Record = { accept: 'green', drop: 'red', reject: 'orange', } async function listRules(): Promise { const r = await apiClient.get('/firewall/rules') if (!isEnvelope(r.data)) return [] return (r.data.data as { rules?: FwRule[] }).rules ?? [] } async function listAO(): Promise { const r = await apiClient.get('/firewall/address-objects') if (!isEnvelope(r.data)) return [] return (r.data.data as { address_objects?: AddressObject[] }).address_objects ?? [] } async function listAG(): Promise { const r = await apiClient.get('/firewall/address-groups') if (!isEnvelope(r.data)) return [] return (r.data.data as { address_groups?: AddressGroup[] }).address_groups ?? [] } async function listSv(): Promise { const r = await apiClient.get('/firewall/services') if (!isEnvelope(r.data)) return [] return (r.data.data as { services?: FwService[] }).services ?? [] } async function listSG(): Promise { const r = await apiClient.get('/firewall/service-groups') if (!isEnvelope(r.data)) return [] return (r.data.data as { service_groups?: ServiceGroup[] }).service_groups ?? [] } async function listZones(): Promise { const r = await apiClient.get('/firewall/zones') if (!isEnvelope(r.data)) return [] return (r.data.data as { zones?: FwZone[] }).zones ?? [] } function buildPayload(v: FormValues) { const out: Partial = { name: v.name, priority: v.priority, enabled: v.enabled, action: v.action, src_zone: v.src_zone, dst_zone: v.dst_zone, log: v.log, comment: v.comment, src_address_object_id: null, src_address_group_id: null, src_cidr: null, dst_address_object_id: null, dst_address_group_id: null, dst_cidr: null, service_object_id: null, service_group_id: null, } if (v.src_kind === 'object') out.src_address_object_id = v.src_address_object_id ?? null if (v.src_kind === 'group') out.src_address_group_id = v.src_address_group_id ?? null if (v.src_kind === 'cidr') out.src_cidr = v.src_cidr ?? null if (v.dst_kind === 'object') out.dst_address_object_id = v.dst_address_object_id ?? null if (v.dst_kind === 'group') out.dst_address_group_id = v.dst_address_group_id ?? null if (v.dst_kind === 'cidr') out.dst_cidr = v.dst_cidr ?? null if (v.service_kind === 'object') out.service_object_id = v.service_object_id ?? null if (v.service_kind === 'group') out.service_group_id = v.service_group_id ?? null return out } export default function RulesTab() { const { t } = useTranslation() const qc = useQueryClient() const { data: rules, isLoading } = useQuery({ queryKey: ['fw', 'rules'], queryFn: listRules }) const { data: aos } = useQuery({ queryKey: ['fw', 'addr-obj'], queryFn: listAO }) const { data: ags } = useQuery({ queryKey: ['fw', 'addr-grp'], queryFn: listAG }) const { data: svs } = useQuery({ queryKey: ['fw', 'svc'], queryFn: listSv }) const { data: sgs } = useQuery({ queryKey: ['fw', 'svc-grp'], queryFn: listSG }) const { data: zones } = useQuery({ queryKey: ['fw', 'zones'], queryFn: listZones }) // Picker options: 'any' (special) + every zone the operator has // declared. Fallback to the seed list while the query is loading. const zoneOptions: Zone[] = zones && zones.length > 0 ? ['any', ...zones.map((z) => z.name)] : ZONES_FALLBACK const aoLabel = (id?: number | null) => aos?.find(o => o.id === id)?.name ?? `#${id}` const agLabel = (id?: number | null) => ags?.find(g => g.id === id)?.name ?? `#${id}` const svLabel = (id?: number | null) => svs?.find(s => s.id === id)?.name ?? `#${id}` const sgLabel = (id?: number | null) => sgs?.find(g => g.id === id)?.name ?? `#${id}` const renderSide = (objID?: number | null, grpID?: number | null, cidr?: string | null) => { if (objID) return obj:{aoLabel(objID)} if (grpID) return grp:{agLabel(grpID)} if (cidr) return {cidr} return any } const renderService = (objID?: number | null, grpID?: number | null) => { if (objID) return {svLabel(objID)} if (grpID) return grp:{sgLabel(grpID)} return any } const [editing, setEditing] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const create = useMutation({ mutationFn: async (v: FormValues) => { await apiClient.post('/firewall/rules', buildPayload(v)) }, onSuccess: () => { message.success(t('common.save')); setCreating(false); form.resetFields() void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) }, }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/rules/${id}`, buildPayload(v)) }, onSuccess: () => { message.success(t('common.save')); setEditing(null); form.resetFields() void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) }, }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/firewall/rules/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) }, }) const editFromRow = (r: FwRule) => { setEditing(r) form.setFieldsValue({ name: r.name ?? undefined, priority: r.priority, enabled: r.enabled, action: r.action, src_zone: r.src_zone, dst_zone: r.dst_zone, log: r.log, comment: r.comment ?? undefined, src_kind: r.src_address_object_id ? 'object' : r.src_address_group_id ? 'group' : r.src_cidr ? 'cidr' : 'any', src_address_object_id: r.src_address_object_id ?? undefined, src_address_group_id: r.src_address_group_id ?? undefined, src_cidr: r.src_cidr ?? undefined, dst_kind: r.dst_address_object_id ? 'object' : r.dst_address_group_id ? 'group' : r.dst_cidr ? 'cidr' : 'any', dst_address_object_id: r.dst_address_object_id ?? undefined, dst_address_group_id: r.dst_address_group_id ?? undefined, dst_cidr: r.dst_cidr ?? undefined, service_kind: r.service_object_id ? 'object' : r.service_group_id ? 'group' : 'any', service_object_id: r.service_object_id ?? undefined, service_group_id: r.service_group_id ?? undefined, }) } const columns: ColumnsType = [ { title: '#', dataIndex: 'priority', key: 'priority', width: 70 }, { title: t('fw.rule.action'), dataIndex: 'action', key: 'action', render: (a: FwRule['action']) => {a.toUpperCase()}, }, { title: t('fw.rule.src'), key: 'src', render: (_, r) => {r.src_zone}{renderSide(r.src_address_object_id, r.src_address_group_id, r.src_cidr)}, }, { title: t('fw.rule.dst'), key: 'dst', render: (_, r) => {r.dst_zone}{renderSide(r.dst_address_object_id, r.dst_address_group_id, r.dst_cidr)}, }, { title: t('fw.rule.service'), key: 'svc', render: (_, r) => renderService(r.service_object_id, r.service_group_id), }, { title: t('fw.rule.enabled'), dataIndex: 'enabled', key: 'enabled', render: (v: boolean) => v ? '✓' : '—' }, { title: t('fw.rule.name'), dataIndex: 'name', key: 'name', render: (v?: string) => v ?? '—' }, { title: t('common.edit'), key: 'actions', render: (_, row) => ( del.mutate(row.id)}> ), }, ] const openCreate = () => { setCreating(true); form.resetFields() form.setFieldsValue({ priority: 100, enabled: true, action: 'accept', log: false, src_zone: 'any', dst_zone: 'any', src_kind: 'any', dst_kind: 'any', service_kind: 'any', }) } return ( <> } title={t('fw.rule.emptyTitle')} description={t('fw.rule.emptyDesc')} action={} /> } /> { setEditing(null); setCreating(false) }} onOk={() => { void form.submit() }} confirmLoading={create.isPending || update.isPending} width={620} >
{ if (editing) update.mutate({ id: editing.id, v }); else create.mutate(v) }} > ({ value: z, label: z }))} /> ({ value: o.id, label: `${o.name} (${o.kind}: ${o.value})` }))} /> } if (k === 'group') { return } return null }} ))} ({ value: s.id, label: `${s.name} (${s.proto}${s.port_start ? ' '+s.port_start : ''})`, }))} /> } if (k === 'group') { return
) }