nft rule post json
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-02-11 21:26:58 +01:00
parent c34b71f5d7
commit 6c5b5ef4a1
9 changed files with 1000 additions and 267 deletions

View File

@@ -1,239 +1,13 @@
// src/components/RulesView.tsx
import { DeleteOutlined, EyeOutlined, ReloadOutlined } from '@ant-design/icons';
import { Button, Card, Collapse, Drawer, Empty, Popconfirm, Space, Spin, Table, Typography, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { deleteRule, fetchRuleset } from '../api/apiClient';
import { ChainOut, RulesetModel, TableOut } from '../types/firewall';
const { Panel } = Collapse;
const { Paragraph, Text } = Typography;
interface RuleRow {
key: string;
handle?: number | null;
text: string;
expr: any;
comment?: string | null;
position?: any;
// context for delete
family: string;
table: string;
chain: string;
}
/**
* Render rules inside an antd Table. The UI layout:
* - Collapse per table (title = family:name)
* - Inside each table -> collapse per chain (only chains that have rules)
* - Each chain shows a Table of rules with delete and inspect actions
*/
export const RulesView: React.FC<{ apiBase?: string }> = ({ apiBase = '' }) => {
const [loading, setLoading] = useState(false);
const [ruleset, setRuleset] = useState<RulesetModel | null>(null);
const [rawFallback, setRawFallback] = useState<string | null>(null);
const [exprDrawerVisible, setExprDrawerVisible] = useState(false);
const [exprForView, setExprForView] = useState<any>(null);
const [selectedRuleContext, setSelectedRuleContext] = useState<{
family: string;
table: string;
chain: string;
handle?: number | null;
} | null>(null);
const fetchRules = useCallback(async () => {
setLoading(true);
try {
const data = await fetchRuleset();
if (!data || data.ruleset === null) {
setRuleset({ tables: [] });
setRawFallback(null);
} else if (typeof data.ruleset === 'string') {
// fallback textual ruleset
setRawFallback(data.ruleset);
setRuleset({ tables: [] });
} else {
setRuleset(data.ruleset as RulesetModel);
setRawFallback(null);
}
} catch (err: any) {
message.error(`Failed to load ruleset: ${err?.message ?? err}`);
setRuleset({ tables: [] });
setRawFallback(null);
} finally {
setLoading(false);
}
}, [apiBase]);
useEffect(() => {
fetchRules();
}, [fetchRules]);
const onDelete = useCallback(
async (row: RuleRow) => {
if (!row.handle) {
message.error('Rule has no handle and cannot be deleted via API.');
return;
}
setLoading(true);
try {
await deleteRule(row.handle, row.family, row.table, row.chain);
message.success('Rule deleted');
await fetchRules();
} catch (err: any) {
message.error(`Delete failed: ${err?.message ?? err}`);
} finally {
setLoading(false);
}
},
[apiBase, fetchRules],
);
const openExprViewer = useCallback((expr: any) => {
setExprForView(expr);
setExprDrawerVisible(true);
}, []);
const closeExprViewer = useCallback(() => {
setExprForView(null);
setExprDrawerVisible(false);
}, []);
// columns for rule table
const columns: ColumnsType<RuleRow> = useMemo(
() => [
{
title: 'Handle',
dataIndex: 'handle',
key: 'handle',
width: 100,
render: (val) => val ?? '-',
},
{
title: 'Rule',
dataIndex: 'text',
key: 'text',
render: (txt: string, rec: RuleRow) => (
<Paragraph copyable={{ text: txt }} style={{ margin: 0 }}>
{txt}
</Paragraph>
),
},
{
title: 'Comment',
dataIndex: 'comment',
key: 'comment',
width: 200,
render: (c) => (c ? <Text>{c}</Text> : null),
},
{
title: 'Actions',
key: 'actions',
width: 160,
align: 'right',
render: (_, rec) => (
<Space>
<Button type="default" icon={<EyeOutlined />} size="small" onClick={() => openExprViewer(rec.expr)}>
View
</Button>
<Popconfirm title="Delete this rule?" onConfirm={() => onDelete(rec)} okText="Delete" cancelText="Cancel">
<Button danger icon={<DeleteOutlined />} size="small">
Delete
</Button>
</Popconfirm>
</Space>
),
},
],
[onDelete, openExprViewer],
);
// flatten rules for each chain into a table datasource
const renderChainTable = (table: TableOut, chain: ChainOut) => {
const data: RuleRow[] = chain.rules.map((r, idx) => ({
key: `${table.family}:${table.name}:${chain.name}:${String(r.handle ?? idx)}`,
handle: r.handle ?? null,
text: r.text,
expr: r.expr,
comment: r.comment ?? null,
position: r.position,
family: table.family,
table: table.name,
chain: chain.name,
}));
return (
<Table columns={columns} dataSource={data} pagination={{ pageSize: 8 }} size="small" rowKey={(rec) => rec.key} />
);
};
// UI when JSON is not available
if (!loading && rawFallback) {
return (
<Card>
<Space direction="vertical" style={{ width: '100%' }}>
<Text strong>Ruleset (raw)</Text>
<Paragraph style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{rawFallback}</Paragraph>
<Button icon={<ReloadOutlined />} onClick={() => fetchRules()}>
Refresh
</Button>
</Space>
</Card>
);
}
import RuleBuilder from '../components/FirewallRuleBuilder';
import RulesView from '../components/FirewallRulesetViewer';
export const Firewall: React.FC = () => {
return (
<Card
title="Firewall rules"
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={() => fetchRules()} loading={loading}>
Refresh
</Button>
</Space>
}
>
{loading ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin size="large" />
</div>
) : ruleset && ruleset.tables.length > 0 ? (
<Collapse accordion>
{ruleset.tables.map((t) => {
// only show chains that have rules
const chainsWithRules = t.chains.filter((c) => c.rules && c.rules.length > 0);
if (chainsWithRules.length === 0) return null;
return (
<Panel header={`${t.family}:${t.name}`} key={`${t.family}:${t.name}`}>
<Collapse>
{chainsWithRules.map((c) => (
<Panel header={`${c.name} — ${c.rules.length} rule(s)`} key={`${t.family}:${t.name}:${c.name}`}>
{renderChainTable(t, c)}
</Panel>
))}
</Collapse>
</Panel>
);
})}
</Collapse>
) : (
<Empty description="No rules found" />
)}
<Drawer
title="Rule expression (JSON)"
placement="right"
width={640}
onClose={closeExprViewer}
open={exprDrawerVisible}
>
<Paragraph>
<Text strong>Expression (raw JSON)</Text>
</Paragraph>
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(exprForView, null, 2)}</pre>
</Drawer>
</Card>
<div className="firewall-page">
<RulesView />
<RuleBuilder />
</div>
);
};
export default RulesView;
export default Firewall;