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

This commit is contained in:
2026-02-11 21:56:08 +01:00
parent 0389dd77aa
commit dfa9a4a343

View File

@@ -29,14 +29,11 @@ interface RuleBuilderProps {
onCreated?: () => Promise<void> | void; onCreated?: () => Promise<void> | void;
} }
/** /* ---------- helpers ---------- */
* Helper: create an nft JSON 'expr' array from form values.
* The produced expr is intentionally conservative and designed to be renderable by the backend's renderer.
*/
function buildExprFromValues(values: any): Expr[] { function buildExprFromValues(values: any): Expr[] {
const expr: Expr[] = []; const expr: Expr[] = [];
// protocol
const protoChoice = values.protocolChoice ?? 'preset'; const protoChoice = values.protocolChoice ?? 'preset';
if (protoChoice === 'preset') { if (protoChoice === 'preset') {
const preset = values.protocolPreset; const preset = values.protocolPreset;
@@ -90,7 +87,6 @@ function buildExprFromValues(values: any): Expr[] {
} }
} }
// source/destination addresses
if (values.saddr) { if (values.saddr) {
expr.push({ expr.push({
match: { match: {
@@ -110,7 +106,6 @@ function buildExprFromValues(values: any): Expr[] {
}); });
} }
// ports for tcp/udp
if (values.sport) { if (values.sport) {
const useUdp = const useUdp =
values.protocolPreset === 'udp' || values.protocolPreset === 'udp' ||
@@ -130,7 +125,6 @@ function buildExprFromValues(values: any): Expr[] {
expr.push(obj); expr.push(obj);
} }
// advanced free-text (try JSON, otherwise string token)
if (values.advanced) { if (values.advanced) {
const adv = values.advanced.trim(); const adv = values.advanced.trim();
try { try {
@@ -141,7 +135,6 @@ function buildExprFromValues(values: any): Expr[] {
} }
} }
// action
const action = values.action || 'drop'; const action = values.action || 'drop';
if (action === 'drop') expr.push({ drop: null }); if (action === 'drop') expr.push({ drop: null });
else if (action === 'accept') expr.push({ accept: null }); else if (action === 'accept') expr.push({ accept: null });
@@ -150,9 +143,6 @@ function buildExprFromValues(values: any): Expr[] {
return expr; return expr;
} }
/**
* Deterministic short textual serializer for expr (preview)
*/
function textFromExpr(expr: Expr): string { function textFromExpr(expr: Expr): string {
if (expr == null) return ''; if (expr == null) return '';
if (Array.isArray(expr)) { if (Array.isArray(expr)) {
@@ -211,12 +201,15 @@ function textFromExpr(expr: Expr): string {
return String(expr); return String(expr);
} }
/* ---------- component ---------- */
export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => { export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [tables, setTables] = useState<TableOut[]>([]); const [tables, setTables] = useState<TableOut[]>([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [preview, setPreview] = useState('');
const [exprPreview, setExprPreview] = useState<Expr | null>(null); const [cmdPreview, setCmdPreview] = useState<string>('');
const [requestJsonPreview, setRequestJsonPreview] = useState<any>(null);
// fetch tables/chains on mount // fetch tables/chains on mount
useEffect(() => { useEffect(() => {
@@ -269,8 +262,35 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
const updatePreviews = useCallback(() => { const updatePreviews = useCallback(() => {
const vals = form.getFieldsValue(); const vals = form.getFieldsValue();
const expr = buildExprFromValues(vals); const expr = buildExprFromValues(vals);
setExprPreview(expr);
setPreview(textFromExpr(expr)); // compute family/table/chain choices for cmd + request JSON
let family = 'inet';
let tableName = 'filter';
let chain = 'input';
const tableSelect = vals.tableSelect;
if (tableSelect && tableSelect !== '__manual__') {
const [f, n] = String(tableSelect).split(':');
family = f || family;
tableName = n || tableName;
} else {
family = vals.familyManual || family;
tableName = vals.tableManual || tableName;
}
const chainVal = vals.chainSelect;
if (!chainVal || chainVal === '__manual_chain__') {
chain = vals.chainManual || chain;
} else {
chain = chainVal;
}
const short = textFromExpr(expr);
const cmd = `add rule ${family} ${tableName} ${chain} ${short}`.trim();
setCmdPreview(cmd);
const reqObj = { family, table: tableName, chain, expr };
setRequestJsonPreview(reqObj);
}, [form]); }, [form]);
useEffect(() => { useEffect(() => {
@@ -302,6 +322,8 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
} }
const expr = buildExprFromValues(values); const expr = buildExprFromValues(values);
const cmd = `add rule ${family} ${tableName} ${chain} ${textFromExpr(expr)}`.trim();
const reqObj = { family, table: tableName, chain, expr };
Modal.confirm({ Modal.confirm({
title: 'Create rule (JSON)', title: 'Create rule (JSON)',
@@ -314,25 +336,23 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</b> </b>
</Text> </Text>
<Divider /> <Divider />
<Text strong>Preview:</Text> <Text strong>Generated nft command:</Text>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{preview || '(no textual preview available)'}</pre> <pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{cmd || '(no textual preview available)'}</pre>
<Divider /> <Divider />
<Text strong>Expression (JSON):</Text> <Text strong>Full JSON NFT request:</Text>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(expr, null, 2)}</pre> <pre style={{ whiteSpace: 'pre-wrap', marginTop: 8 }}>{JSON.stringify(reqObj, null, 2)}</pre>
</div> </div>
), ),
okText: 'Create', okText: 'Create',
onOk: async () => { onOk: async () => {
setLoading(true); setLoading(true);
try { try {
const req = { family, table: tableName, chain, expr }; const res = await createRuleJson(reqObj);
const res = await createRuleJson(req);
if (res && res.rc === 0) { if (res && res.rc === 0) {
message.success('Rule created'); message.success('Rule created');
if (onCreated) await onCreated(); if (onCreated) await onCreated();
form.resetFields(['advanced']); form.resetFields(['advanced']);
} else { } else {
// server returned 2xx but rc != 0
message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`); message.error(`Create failed: ${res?.stderr ?? 'unknown error'}`);
} }
} catch (err: any) { } catch (err: any) {
@@ -366,7 +386,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
}, },
}); });
}, },
[form, preview, onCreated], [form, onCreated],
); );
// prepare chain options for currently selected table // prepare chain options for currently selected table
@@ -412,6 +432,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
onFinish={handleCreate} onFinish={handleCreate}
onValuesChange={onValuesChange} onValuesChange={onValuesChange}
> >
{/* Table 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 }]}>
@@ -446,6 +467,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</Col> </Col>
</Row> </Row>
{/* Chain selection */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={12}> <Col xs={24} sm={12}>
<Form.Item name="chainSelect" label="Chain (select)"> <Form.Item name="chainSelect" label="Chain (select)">
@@ -467,6 +489,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</Col> </Col>
</Row> </Row>
{/* Protocol / addresses */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={8}> <Col xs={24} sm={8}>
<Form.Item label="Protocol selection" style={{ marginBottom: 0 }}> <Form.Item label="Protocol selection" style={{ marginBottom: 0 }}>
@@ -510,6 +533,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</Col> </Col>
</Row> </Row>
{/* Ports / action */}
<Row gutter={16}> <Row gutter={16}>
<Col xs={24} sm={6}> <Col xs={24} sm={6}>
<Form.Item name="sport" label="Source Port"> <Form.Item name="sport" label="Source Port">
@@ -532,6 +556,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</Col> </Col>
</Row> </Row>
{/* Advanced */}
<Row> <Row>
<Col span={24}> <Col span={24}>
<Form.Item name="advanced" label="Advanced (optional)"> <Form.Item name="advanced" label="Advanced (optional)">
@@ -551,22 +576,28 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
<Form.Item label="Preview"> <Form.Item label="Preview">
<Space style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Space style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1, marginRight: 12 }}> <div style={{ flex: 1, marginRight: 12 }}>
<Text strong>Short preview:</Text> <Text strong>nft command preview:</Text>
<div style={{ marginTop: 6 }}> <div style={{ marginTop: 6 }}>
<Text code copyable={{ text: preview || '' }}> <Text code copyable={{ text: cmdPreview || '' }}>
{preview || '(no textual preview available)'} {cmdPreview || '(no textual preview available)'}
</Text> </Text>
</div> </div>
</div> </div>
<div style={{ width: 320 }}> <div style={{ width: 320 }}>
<Tooltip title="Copy JSON expr"> <Tooltip title="Click to copy expr JSON. Shift+click to copy full request JSON.">
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
size="small" size="small"
onClick={() => { onClick={(e) => {
if (!exprPreview) return; // shiftKey => copy full request JSON
navigator.clipboard?.writeText(JSON.stringify(exprPreview, null, 2)); if (e.shiftKey && requestJsonPreview) {
navigator.clipboard?.writeText(JSON.stringify(requestJsonPreview, null, 2));
message.success('Full request JSON copied');
return;
}
if (!cmdPreview) return;
navigator.clipboard?.writeText(JSON.stringify(cmdPreview, null, 2));
message.success('Expression JSON copied'); message.success('Expression JSON copied');
}} }}
/> />
@@ -580,9 +611,9 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = ({ onCreated }) => {
</Form> </Form>
<Divider /> <Divider />
<Text strong>Expression (JSON):</Text> <Text strong>Full JSON NFT request:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#f6f6f6', padding: 12 }}> <pre style={{ whiteSpace: 'pre-wrap', background: '#f6f6f6', padding: 12 }}>
{JSON.stringify(exprPreview, null, 2)} {JSON.stringify(requestJsonPreview, null, 2)}
</pre> </pre>
</Card> </Card>
); );