GOOD RAW
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-02-28 18:41:25 +01:00
parent 64d3a973bc
commit 7e89dcd8a1
2 changed files with 180 additions and 248 deletions

View File

@@ -111,7 +111,7 @@ export default function FirewallAddChainModal({
values && typeof values.chainName === 'string' && values.chainName.trim().length > 0 values && typeof values.chainName === 'string' && values.chainName.trim().length > 0
? values.chainName.trim() ? values.chainName.trim()
: undefined; : undefined;
const chainFromForm = form.getFieldValue('chainName'); const chainFromForm = form.getFieldValue('hook');
const chain = const chain =
chainFromValues || chainFromValues ||
(typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) || (typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) ||
@@ -287,12 +287,6 @@ export default function FirewallAddChainModal({
</Form.Item> </Form.Item>
)} )}
</Col> </Col>
<Col span={8}>
<Form.Item name="chainName" label="Chain Name" rules={[{ required: true }]}>
<Input placeholder="e.g. forward" />
</Form.Item>
</Col>
</Row> </Row>
<Row gutter={12} style={{ marginTop: 8 }}> <Row gutter={12} style={{ marginTop: 8 }}>

View File

@@ -18,16 +18,13 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createRuleJson } from '../api/apiClient'; import { execFirewallRaw } from '../api/apiClient';
import { ChainOut, Expr, RuleOut, TableOut } from '../types/firewall'; import type { ChainOut, ExecResult, Expr, RuleOut, TableOut } from '../types/firewall';
const { Option } = Select; const { Option } = Select;
const { TextArea } = Input; const { TextArea } = Input;
const { Title, Text } = Typography; const { Title, Text } = Typography;
const MANUAL_TABLE = '__manual__';
const MANUAL_CHAIN = '__manual_chain__';
type FormValues = Record<string, any>; type FormValues = Record<string, any>;
interface RuleBuilderProps { interface RuleBuilderProps {
@@ -39,11 +36,17 @@ interface RuleBuilderProps {
onRulesChange?: (tables: TableOut[]) => void; onRulesChange?: (tables: TableOut[]) => void;
} }
/* Helpers (unchanged) */ /* ----------------------
Small helper functions
---------------------- */
function isPresetProtocolChoice(values: FormValues) { function isPresetProtocolChoice(values: FormValues) {
return (values.protocolChoice ?? 'preset') === 'preset'; return (values.protocolChoice ?? 'preset') === 'preset';
} }
/**
* Build NFT expression tokens (keeps your original logic).
*/
export function buildExprFromValues(values: FormValues): Expr[] { export function buildExprFromValues(values: FormValues): Expr[] {
const expr: Expr[] = []; const expr: Expr[] = [];
@@ -156,6 +159,9 @@ export function buildExprFromValues(values: FormValues): Expr[] {
return expr; return expr;
} }
/**
* Create a compact textual representation of an expr.
*/
export function textFromExpr(expr: Expr | null | undefined): string { export function textFromExpr(expr: Expr | null | undefined): string {
if (!expr) return ''; if (!expr) return '';
if (!Array.isArray(expr)) return String(expr); if (!Array.isArray(expr)) return String(expr);
@@ -230,52 +236,78 @@ export function textFromExpr(expr: Expr | null | undefined): string {
return tokens.join(' '); return tokens.join(' ');
} }
/* Component */ /* -------------------------
Command generation helper
------------------------- */
function generateCommandFromValues(values: FormValues): { cmd: string; textual: string; position?: number | null } {
const expr = buildExprFromValues(values);
let textual = textFromExpr(expr).trim();
if (values.advanced && typeof values.advanced === 'string' && values.advanced.trim() !== '') {
textual = `${textual} ${values.advanced.trim()}`.trim();
}
if (values.action === 'nfqueue' || values.action === 'queue') {
const qnum = values.nfqueue ?? values.queue ?? null;
const qstr = qnum != null && String(qnum).trim() !== '' ? String(qnum).trim() : null;
const hasQueue = /\bqueue(?:\s+num)?\b/i.test(textual);
if (qstr) {
if (!hasQueue) textual = `${textual} queue num ${qstr}`.trim();
else textual = textual.replace(/\bqueue(?:\s+num)?\s*\d*/i, `queue num ${qstr}`);
} else {
if (!hasQueue) textual = `${textual} queue num 0`.trim();
else textual = textual.replace(/\bqueue(?:\s+num)?\s*\d*/i, `queue num 0`);
}
}
const tableSelect = values.tableSelect;
const chain = values.chainSelect || 'input';
const [family = 'inet', table = 'filter'] = tableSelect ? String(tableSelect).split(':') : ['inet', 'filter'];
const before = values.insertBeforeHandle;
const hasBefore = before != null && String(before) !== '';
const verb = hasBefore ? 'insert' : 'add';
const positionPart = hasBefore ? ` position ${before}` : '';
const cmd = `${verb} rule ${family} ${table} ${chain}${positionPart} ${textual}`.replace(/\s+/g, ' ').trim();
return { cmd, textual, position: hasBefore ? Number(before) : undefined };
}
/* -------------------------
Component
------------------------- */
export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps) => { export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cmdPreview, setCmdPreview] = useState(''); const [cmdPreview, setCmdPreview] = useState('');
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
// state for insert-before options (so we can recompute on both table change and form change)
const [insertBeforeOptions, setInsertBeforeOptions] = useState<Array<{ value: any; label: string }>>([]); const [insertBeforeOptions, setInsertBeforeOptions] = useState<Array<{ value: any; label: string }>>([]);
// Resolve table selection convenience
const resolveTable = useCallback((values: FormValues) => {
const tableSelect = values.tableSelect;
if (!tableSelect || tableSelect === MANUAL_TABLE) {
return {
family: values.familyManual || 'inet',
table: values.tableManual || 'filter',
};
}
const [family = 'inet', table = 'filter'] = String(tableSelect).split(':');
return { family, table };
}, []);
const tableOptions = useMemo( const tableOptions = useMemo(
() => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })), () => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })),
[props.tables], [props.tables],
); );
// compute insert-before options from current form selection + props.tables const noTables = !(props.tables && props.tables.length > 0);
const updateInsertOptions = useCallback(() => { const updateInsertOptions = useCallback(() => {
const ts = form.getFieldValue('tableSelect'); const ts = form.getFieldValue('tableSelect');
const cs = form.getFieldValue('chainSelect'); const cs = form.getFieldValue('chainSelect');
if (!ts || ts === MANUAL_TABLE || !cs || cs === MANUAL_CHAIN) { if (!ts || !cs) {
setInsertBeforeOptions([]); setInsertBeforeOptions([]);
return; return;
} }
const [family, table] = String(ts).split(':'); const [family, table] = String(ts).split(':');
const tbl = props.tables?.find((t) => t.family === family && t.name === table); const tbl = props.tables?.find((t) => t.family === family && t.name === table);
if (!tbl) { if (!tbl) {
setInsertBeforeOptions([]); setInsertBeforeOptions([]);
return; return;
} }
const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs); const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs);
if (!ch || !Array.isArray(ch.rules)) { if (!ch || !Array.isArray(ch.rules)) {
setInsertBeforeOptions([]); setInsertBeforeOptions([]);
@@ -286,13 +318,22 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
.filter((r: RuleOut) => r && r.handle != null) .filter((r: RuleOut) => r && r.handle != null)
.map((r: RuleOut) => ({ .map((r: RuleOut) => ({
value: r.handle, value: r.handle,
label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))}`, label: `#${r.handle} — ${
r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))
}`,
})); }));
setInsertBeforeOptions(opts); setInsertBeforeOptions(opts);
}, [form, props.tables]); }, [form, props.tables]);
// When the (parent-supplied) tables change, pick sensible defaults and update previews + insert options. const updatePreviewFromValues = useCallback(
(values?: FormValues) => {
const vals = values ?? form.getFieldsValue();
const { cmd } = generateCommandFromValues(vals);
setCmdPreview(cmd);
},
[form],
);
useEffect(() => { useEffect(() => {
if (tableOptions.length > 0) { if (tableOptions.length > 0) {
const first = tableOptions[0].value; const first = tableOptions[0].value;
@@ -301,6 +342,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
protocolChoice: 'preset', protocolChoice: 'preset',
protocolPreset: 'icmp', protocolPreset: 'icmp',
action: 'drop', action: 'drop',
nfqueue: 1,
}); });
const [f, n] = String(first).split(':'); const [f, n] = String(first).split(':');
@@ -308,179 +350,117 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
if (tbl && tbl.chains && tbl.chains.length > 0) { if (tbl && tbl.chains && tbl.chains.length > 0) {
form.setFieldsValue({ chainSelect: tbl.chains[0].name }); form.setFieldsValue({ chainSelect: tbl.chains[0].name });
} else { } else {
form.setFieldsValue({ chainSelect: MANUAL_CHAIN }); form.setFieldsValue({ chainSelect: undefined });
} }
// recompute insert-before options after defaults set
// give AntD a tick to flush form fields, then compute options & previews
setTimeout(() => { setTimeout(() => {
updateInsertOptions(); updateInsertOptions();
updatePreviews(); updatePreviewFromValues();
}, 0); }, 0);
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
tableSelect: MANUAL_TABLE,
protocolChoice: 'preset', protocolChoice: 'preset',
protocolPreset: 'icmp', protocolPreset: 'icmp',
action: 'drop', action: 'drop',
chainSelect: MANUAL_CHAIN, nfqueue: 1,
tableSelect: undefined,
chainSelect: undefined,
}); });
setInsertBeforeOptions([]); setInsertBeforeOptions([]);
setTimeout(() => updatePreviews(), 0); setTimeout(() => updatePreviewFromValues(), 0);
} }
// only when parent tables change // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.tables, tableOptions.length, updateInsertOptions]); }, [props.tables, tableOptions.length]);
// preview update (reads form values)
const updatePreviews = useCallback(() => {
const values = form.getFieldsValue();
const expr = buildExprFromValues(values);
const { family, table } = resolveTable(values);
let chain = 'input';
const chainVal = values.chainSelect;
if (!chainVal || chainVal === MANUAL_CHAIN) chain = values.chainManual || chain;
else chain = chainVal;
const beforeHandle = values.insertBeforeHandle;
const short = textFromExpr(expr);
const cmdFriendly = beforeHandle
? `add rule ${family} ${table} ${chain} position ${beforeHandle} ${short}`
: `add rule ${family} ${table} ${chain} ${short}`;
setCmdPreview(cmdFriendly);
const req: any = { family, table, chain, expr };
if (beforeHandle != null && beforeHandle !== '') {
// backend expects "position"
req.position = Number(beforeHandle);
}
setRequestJsonPreview(req);
}, [form, resolveTable]);
// run once at mount to compute previews from initial values
useEffect(() => { useEffect(() => {
updatePreviews(); const current = form.getFieldsValue();
if (current.nfqueue == null) form.setFieldsValue({ nfqueue: 1 });
updatePreviewFromValues();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// when user changes form fields const onValuesChange = useCallback(
const onValuesChange = useCallback(() => { (_: any, allValues: FormValues) => {
updatePreviews(); // If user switches to NFQUEUE and nfqueue is empty, seed it with 1
if (allValues.action === 'nfqueue' && (allValues.nfqueue == null || allValues.nfqueue === '')) {
form.setFieldsValue({ nfqueue: 1 });
allValues.nfqueue = 1;
}
// keep chain in sync when tableSelect changes const ts = allValues.tableSelect;
const values = form.getFieldsValue(); if (ts) {
const ts = values.tableSelect; const [f, n] = String(ts).split(':');
if (ts && ts !== MANUAL_TABLE) { const tbl = props.tables?.find((t) => t.family === f && t.name === n);
const [f, n] = String(ts).split(':'); if (tbl) {
const tbl = props.tables?.find((t) => t.family === f && t.name === n); if (tbl.chains && tbl.chains.length > 0) {
if (tbl) { if (!allValues.chainSelect) {
if (tbl.chains && tbl.chains.length > 0) { form.setFieldsValue({ chainSelect: tbl.chains[0].name });
const currentChain = form.getFieldValue('chainSelect'); }
if (!currentChain || currentChain === MANUAL_CHAIN) { } else {
form.setFieldsValue({ chainSelect: tbl.chains[0].name }); form.setFieldsValue({ chainSelect: undefined });
} }
} else {
form.setFieldsValue({ chainSelect: MANUAL_CHAIN });
} }
} }
}
// recompute insert-before options when the user changes table/chain updateInsertOptions();
updateInsertOptions(); updatePreviewFromValues(allValues);
}, [form, props.tables, updatePreviews, updateInsertOptions]); },
[form, props.tables, updateInsertOptions, updatePreviewFromValues],
);
// create handler — component will call props.refreshRules if provided; otherwise it will NOT fetch rules itself.
const handleCreate = useCallback( const handleCreate = useCallback(
async (values: FormValues) => { async (values: FormValues) => {
const { family, table } = resolveTable(values); const { cmd } = generateCommandFromValues(values);
const chain =
!values.chainSelect || values.chainSelect === MANUAL_CHAIN ? values.chainManual || 'input' : values.chainSelect;
const expr = buildExprFromValues(values);
const req: any = { family, table, chain, expr };
if (values.insertBeforeHandle != null && values.insertBeforeHandle !== '') {
// backend expects "position" (per your request)
req.position = Number(values.insertBeforeHandle);
}
const textual = textFromExpr(expr);
const previewHint = req.position ? ` ${req.position}` : '';
const cmd = `add rule ${family} ${table} ${chain}${previewHint} ${textual}`.trim();
Modal.confirm({ Modal.confirm({
title: 'Create rule (JSON)', title: 'Run raw nft command',
content: ( content: (
<div> <div>
<Text> <Text>
About to create rule in{' '} About to run nft command in <b>{String(values.tableSelect ?? 'inet:filter')}</b> (see preview).
<b>
{family}:{table} {chain}
</b>
</Text> </Text>
<Divider /> <Divider />
<Text strong>Generated nft command:</Text> <Text strong>Command:</Text>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd || '(no textual preview available)'}</pre> <pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd}</pre>
<Divider />
<Text strong>Full JSON NFT request:</Text>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(req, null, 2)}</pre>
</div> </div>
), ),
okText: 'Create', okText: 'Run',
onOk: async () => { onOk: async () => {
setLoading(true); setLoading(true);
try { try {
const res = await createRuleJson(req); const out: ExecResult = await execFirewallRaw(cmd);
if (res && res.rc === 0) { const stderrText = out?.stderr ? String(out.stderr).trim() : '';
message.success('Rule created'); if (stderrText) {
if (props.refreshRules) { message.error(`Command produced stderr: ${stderrText}`);
await props.refreshRules(); } else if (out && (out.rc === 0 || out.rc === -1)) {
} else { message.success('Command executed (rc ' + out.rc + ')');
message.info('Parent did not provide a refresh function; please refresh ruleset externally.'); if (props.refreshRules) await props.refreshRules();
}
if (props.onCreated) await props.onCreated(); if (props.onCreated) await props.onCreated();
} else { } else {
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`); const info = out
? `rc:${out.rc}` +
(out.stdout ? ` stdout:${out.stdout}` : '') +
(out.stderr ? ` stderr:${out.stderr}` : '')
: 'unknown result';
message.error('Command failed: ' + info);
} }
} catch (err: any) { } catch (err: any) {
const resp = err?.response; message.error('Execution failed: ' + (err?.message ?? String(err)));
if (resp && resp.data) {
const data = resp.data;
if (typeof data === 'object' && (typeof data.rc === 'number' || 'stderr' in data)) {
const rc = Number(data.rc ?? -1);
const stderr = data.stderr ?? data;
if (rc === 0) {
message.warn('Rule may have been created; check ruleset.');
if (props.refreshRules) {
await props.refreshRules();
}
} else {
const errMsg = typeof stderr === 'string' ? stderr : JSON.stringify(stderr);
message.error(`Create failed: ${errMsg}`);
}
} else if (resp.data.detail) {
message.error(`Create failed: ${resp.data.detail}`);
} else {
message.error(`Create failed: ${JSON.stringify(resp.data)}`);
}
} else {
message.error(`Create failed: ${err?.message ?? String(err)}`);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, },
}); });
}, },
[form, props.refreshRules, resolveTable], [props.refreshRules, props.onCreated],
); );
const chainOptions = useMemo(() => { const chainOptions = useMemo(() => {
const ts = form.getFieldValue('tableSelect'); const ts = form.getFieldValue('tableSelect');
if (!ts || ts === MANUAL_TABLE) return null; if (!ts) return [];
const [f, n] = String(ts).split(':'); const [f, n] = String(ts).split(':');
const tbl = props.tables?.find((t) => t.family === f && t.name === n); const tbl = props.tables?.find((t) => t.family === f && t.name === n);
if (!tbl) return null; if (!tbl) return [];
return tbl.chains.map((c) => ( return tbl.chains.map((c) => (
<Option key={c.name} value={c.name}> <Option key={c.name} value={c.name}>
{c.name} {c.name}
@@ -501,8 +481,6 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
console.warn('refresh failed', err); console.warn('refresh failed', err);
message.error('Refresh failed'); message.error('Refresh failed');
} finally { } finally {
// parent should update props.tables — update options once props.tables changes
// but ensure we recompute in case parent updated in place
updateInsertOptions(); updateInsertOptions();
setRefreshing(false); setRefreshing(false);
} }
@@ -513,11 +491,9 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
<Row justify="space-between" align="middle"> <Row justify="space-between" align="middle">
<Col> <Col>
<Title level={4} style={{ margin: 0 }}> <Title level={4} style={{ margin: 0 }}>
Add Firewall Rule (JSON) Add Firewall Rule (raw)
</Title> </Title>
<Text type="secondary"> <Text type="secondary">Builds a raw nft command and executes it via the raw endpoint.</Text>
Builds an nft JSON <code>expr</code> and sends it to the server. If rendering fails, use the Raw UI.
</Text>
</Col> </Col>
<Col> <Col>
@@ -538,103 +514,66 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
protocolChoice: 'preset', protocolChoice: 'preset',
protocolPreset: 'icmp', protocolPreset: 'icmp',
action: 'drop', action: 'drop',
tableSelect: tableOptions.length > 0 ? tableOptions[0].value : MANUAL_TABLE, nfqueue: 1,
tableSelect: tableOptions.length > 0 ? tableOptions[0].value : undefined,
}} }}
onFinish={handleCreate} onFinish={handleCreate}
onValuesChange={onValuesChange} onValuesChange={onValuesChange}
> >
{/* Table selection */} {/* Table & Chain selection */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Form.Item name="tableSelect" label="Table (family:name)" rules={[{ required: true }]}> <Form.Item name="tableSelect" label="Table (family:name)" rules={[{ required: true }]}>
<Select> <Select placeholder={noTables ? 'No tables available' : 'Select table'} disabled={noTables}>
{tableOptions.map((t) => ( {tableOptions.map((t) => (
<Option key={t.value} value={t.value}> <Option key={t.value} value={t.value}>
{t.label} {t.label}
</Option> </Option>
))} ))}
<Option key={MANUAL_TABLE} value={MANUAL_TABLE}>
Manual / new table...
</Option>
</Select> </Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
{form.getFieldValue('tableSelect') === MANUAL_TABLE ? ( <Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}>
<Space style={{ width: '100%' }}> <Select placeholder="Select chain" disabled={noTables || chainOptions.length === 0}>
<Form.Item name="familyManual" label="Family" style={{ flex: 1 }}> {chainOptions}
<Input placeholder="inet, bridge, ip, ip6 ..." /> </Select>
</Form.Item> </Form.Item>
<Form.Item name="tableManual" label="Table" style={{ flex: 1 }}>
<Input placeholder="filter" />
</Form.Item>
</Space>
) : (
<Form.Item label="Table (preview)">
<Input value={form.getFieldValue('tableSelect')} readOnly />
</Form.Item>
)}
</Col> </Col>
</Row> </Row>
{/* Chain selection */} {/* Insert-before */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Form.Item name="chainSelect" label="Chain (select)" rules={[{ required: true }]}> <Form.Item
<Select> name="insertBeforeHandle"
{chainOptions ?? ( label="Insert before (optional)"
<Option key={MANUAL_CHAIN} value={MANUAL_CHAIN}> help="Pick an existing rule handle to insert the new rule before. If left empty the rule will be appended."
Manual chain... >
<Select allowClear placeholder="Append (no insert-before)">
{insertBeforeOptions.length === 0 ? (
<Option value="__none__" disabled>
(no rules available)
</Option> </Option>
) : (
insertBeforeOptions.map((o) => (
<Option key={String(o.value)} value={o.value}>
{o.label}
</Option>
))
)} )}
</Select> </Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col xs={24} sm={12}> <Col xs={24} sm={12} style={{ display: 'flex', alignItems: 'flex-end' }}>
{form.getFieldValue('chainSelect') === MANUAL_CHAIN || <Text type="secondary">
form.getFieldValue('tableSelect') === MANUAL_TABLE ? ( Use when you want the new rule to appear before a known handle. Refresh tables to see latest handles.
<Form.Item name="chainManual" label="Chain (manual)"> </Text>
<Input placeholder="input, forward, output" />
</Form.Item>
) : (
<Form.Item label="Chain (preview)">
<Input value={form.getFieldValue('chainSelect') ?? ''} readOnly />
</Form.Item>
)}
</Col> </Col>
</Row> </Row>
{/* Insert-before control */}
{form.getFieldValue('tableSelect') !== MANUAL_TABLE &&
form.getFieldValue('chainSelect') &&
form.getFieldValue('chainSelect') !== MANUAL_CHAIN && (
<Row gutter={16}>
<Col xs={24} sm={12}>
<Form.Item
name="insertBeforeHandle"
label="Insert before (optional)"
help="Pick an existing rule handle to insert the new rule before. If left empty the rule will be appended."
>
<Select allowClear placeholder="Append (no insert-before)">
{insertBeforeOptions.length === 0 ? (
<Option value="__none__" disabled>
(no rules available)
</Option>
) : (
insertBeforeOptions.map((o) => (
<Option key={String(o.value)} value={o.value}>
{o.label}
</Option>
))
)}
</Select>
</Form.Item>
</Col>
</Row>
)}
{/* Protocol / addresses */} {/* Protocol / addresses */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={8}> <Col xs={24} sm={8}>
@@ -703,8 +642,20 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
{/* NFQUEUE: required when selected, default 1 */}
{form.getFieldValue('action') === 'nfqueue' && ( {form.getFieldValue('action') === 'nfqueue' && (
<Form.Item name="nfqueue" label="Queue number" initialValue={1}> <Form.Item
name="nfqueue"
label="Queue number"
rules={[
{ required: true, message: 'Queue number is required when NFQUEUE is selected' },
{
type: 'number',
min: 1,
message: 'Queue number must be at least 1',
},
]}
>
<InputNumber style={{ width: '100%' }} min={1} max={65535} /> <InputNumber style={{ width: '100%' }} min={1} max={65535} />
</Form.Item> </Form.Item>
)} )}
@@ -717,10 +668,10 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
<Form.Item name="advanced" label="Advanced (optional)"> <Form.Item name="advanced" label="Advanced (optional)">
<TextArea <TextArea
rows={3} rows={3}
placeholder='Append arbitrary JSON token or text (advanced). Example: {"limit": {"rate":"5/second"}}' placeholder='Append advanced text (will be appended to the command preview). Example: "limit rate 5/second"'
/> />
<Text type="secondary"> <Text type="secondary">
Advanced is appended to the expr as a token. Prefer structured fields where possible. Advanced is appended to the textual expr preview. Prefer structured fields where possible.
</Text> </Text>
</Form.Item> </Form.Item>
</Col> </Col>
@@ -740,39 +691,26 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
</div> </div>
<div style={{ width: 320 }}> <div style={{ width: 320 }}>
<Tooltip title="Click to copy expr JSON. Shift+Click copies full request JSON."> <Tooltip title="Copy command to clipboard">
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
size="small" size="small"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={(e: any) => { onClick={() => {
if (e.shiftKey && requestJsonPreview) {
navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
message.success('Full request JSON copied');
return;
}
if (!cmdPreview) return; if (!cmdPreview) return;
navigator.clipboard?.writeText(cmdPreview); navigator.clipboard?.writeText(cmdPreview);
message.success('Command preview copied'); message.success('Command copied');
}} }}
/> />
</Tooltip> </Tooltip>
<Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />}> <Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />} disabled={noTables}>
Create rule Run command
</Button> </Button>
</div> </div>
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>
<Divider />
<Text strong>Full JSON NFT request:</Text>
<div style={{ marginTop: 8 }}>
<Card size="small" type="inner" style={{ background: '#fafafa' }}>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{JSON.stringify(requestJsonPreview, null, 2)}</pre>
</Card>
</div>
</Card> </Card>
); );
}; };