import { useState } from 'react' import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Tag, Tooltip, message } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import DataTable from '../../components/DataTable' import apiClient, { isEnvelope } from '../../api/client' import type { FwService } from './types' interface FormValues { name: string proto: FwService['proto'] port_start?: number port_end?: number description?: string } async function listServices(): Promise { const r = await apiClient.get('/firewall/services') if (!isEnvelope(r.data)) return [] return (r.data.data as { services?: FwService[] }).services ?? [] } export default function ServicesTab() { const { t } = useTranslation() const qc = useQueryClient() const { data, isLoading } = useQuery({ queryKey: ['fw', 'svc'], queryFn: listServices }) 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/services', v) }, onSuccess: () => { message.success(t('common.save')); setCreating(false); form.resetFields() void qc.invalidateQueries({ queryKey: ['fw', 'svc'] }) }, }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/services/${id}`, v) }, onSuccess: () => { message.success(t('common.save')); setEditing(null); form.resetFields() void qc.invalidateQueries({ queryKey: ['fw', 'svc'] }) }, }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/firewall/services/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'svc'] }) }, }) const columns: ColumnsType = [ { title: t('fw.svc.name'), dataIndex: 'name', key: 'name', render: (s: string, row) => {s}{row.builtin && builtin}, }, { title: t('fw.svc.proto'), dataIndex: 'proto', key: 'proto', render: (p: string) => {p} }, { title: t('fw.svc.ports'), key: 'ports', render: (_, row) => row.proto === 'tcp' || row.proto === 'udp' ? {row.port_start === row.port_end ? row.port_start : `${row.port_start}-${row.port_end}`} : '—', }, { title: t('fw.svc.description'), dataIndex: 'description', key: 'desc', render: (v?: string) => v ?? '—' }, { title: t('common.edit'), key: 'actions', render: (_, row) => ( del.mutate(row.id)} disabled={row.builtin} > ), }, ] return ( <> { setEditing(null); setCreating(false) }} onOk={() => { void form.submit() }} confirmLoading={create.isPending || update.isPending} >
{ // For non-tcp/udp protocols, leave ports empty const cleaned = (v.proto === 'tcp' || v.proto === 'udp') ? v : { ...v, port_start: undefined, port_end: undefined } if (editing) update.mutate({ id: editing.id, v: cleaned }); else create.mutate(cleaned) }} >
) }