import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, notification } from 'antd'; import { ReactElement, useEffect, useState } from 'react'; import { execFirewallRaw, fetchRuleset } from '../api/apiClient'; const { Paragraph, Text } = Typography; const { Option } = Select; type ExecResult = { rc: number; stdout?: string | null; stderr?: string | null; }; type CmdResult = { cmd: string; out?: ExecResult; err?: string; }; type Props = { open: boolean; onClose?: (created?: boolean) => void; // created === true when a table was created startOnPreview?: boolean; }; const FAMILY_DESCRIPTIONS: Record = { ip: 'IPv4 packets.', ip6: 'IPv6 packets.', inet: 'Both IPv4 and IPv6.', arp: 'ARP packets.', bridge: 'Packets passing through a bridge.', netdev: 'Direct filtering on a network device (ingress).', }; export default function FirewallAddTableModal({ open, onClose, startOnPreview = false }: Props): ReactElement { const [form] = Form.useForm(); const [localLoadingRuleset, setLocalLoadingRuleset] = useState(false); const [rulesetEmpty, setRulesetEmpty] = useState(false); const [step, setStep] = useState(startOnPreview ? 1 : 0); const [running, setRunning] = useState(false); const [results, setResults] = useState([]); const [selectedFamily, setSelectedFamily] = useState('bridge'); useEffect(() => { form.setFieldsValue({ family: 'bridge', tableName: 'filter', }); setSelectedFamily('bridge'); if (open) { refreshRuleset(); setResults([]); setRunning(false); setStep(startOnPreview ? 1 : 0); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); async function refreshRuleset() { setLocalLoadingRuleset(true); try { const res = await fetchRuleset(); const ruleset = (res as any).ruleset; if (ruleset && Array.isArray(ruleset.tables)) { setRulesetEmpty(ruleset.tables.length === 0); } else { setRulesetEmpty(false); } } catch (err: any) { notification.warning({ message: 'Could not load ruleset', description: err?.message ?? String(err), duration: 6, }); setRulesetEmpty(false); } finally { setLocalLoadingRuleset(false); } } function buildCommands(values: any): string[] { const family = (values.family ?? 'bridge').trim(); const table = (values.tableName ?? 'filter').trim(); return [`add table ${family} ${table}`]; } async function executeCommands(cmds: string[]) { setRunning(true); setResults([]); const acc: CmdResult[] = []; for (const cmd of cmds) { try { const out = (await execFirewallRaw(cmd)) as ExecResult; const success = out && (out.rc === 0 || out.rc === -1); if (success) acc.push({ cmd, out }); else acc.push({ cmd, out, err: `rc: ${out.rc} stderr: ${out.stderr ?? ''}` }); } catch (err: any) { acc.push({ cmd, err: err?.message ?? String(err) }); } } setResults(acc); setRunning(false); try { await refreshRuleset(); } catch { } const hadError = acc.some((r) => r.err); if (!hadError) { notification.success({ message: 'Table created', description: 'Table was created and ruleset has been refreshed locally in the modal.', duration: 4, }); onClose?.(true); // signal parent to refresh and close modal } else { notification.error({ message: 'Some commands returned errors', description: 'See execution results below for details.', duration: 6, }); setStep(1); } } function renderFooter() { return ( {step === 0 ? ( ) : ( )} ); } if (localLoadingRuleset) { return ( onClose?.(false)} footer={null} width={700}>
); } return ( onClose?.(false)} width={700} footer={renderFooter()} destroyOnClose > {step === 0 && (
{ if (changed.family) setSelectedFamily(changed.family); }} > {FAMILY_DESCRIPTIONS[selectedFamily]}
)} {step === 1 && ( <> The command below will be executed on the server (server will prefix with nft):
              {buildCommands(form.getFieldsValue()).join('\n')}
            
{results.length > 0 && ( {results.map((r, i) => (
{r.cmd} {r.err ? ( {r.err} ) : ( <> rc: {r.out?.rc} {r.out?.stdout ? ( <> stdout:
{r.out.stdout}
) : null} {r.out?.stderr ? ( <> stderr:
{r.out.stderr}
) : null} )}
))}
)} )}
); }