JSON WORKS HERE TAG
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
This commit is contained in:
@@ -1,123 +1,30 @@
|
||||
// src/components/FirewallTables.tsx
|
||||
import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Card, Divider, message, Modal, Space, Spin, Table, Typography } from 'antd';
|
||||
import { ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Card, Divider, message, Modal, Space, Table, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import { execFirewallRaw, fetchRuleset, moveRule } from '../api/apiClient';
|
||||
import type { CmdResult, ExecResult } from '../types/firewall';
|
||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { execFirewallRaw } from '../api/apiClient';
|
||||
import type { CmdResult, ExecResult, RuleOut, TableOut } from '../types/firewall';
|
||||
import FirewallAddChainModal from './FireWallAddChainModal';
|
||||
import FirewallAddTableModal from './FireWallAddTableModal';
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
/* Types */
|
||||
type NFTRule = {
|
||||
handle?: number | string;
|
||||
expr?: any;
|
||||
rule?: any;
|
||||
text?: string;
|
||||
[k: string]: any;
|
||||
};
|
||||
/* ---------- Helpers ---------- */
|
||||
|
||||
type NFTChain = {
|
||||
name: string;
|
||||
type?: string | null;
|
||||
hook?: string | null;
|
||||
priority?: number | null;
|
||||
policy?: string | null;
|
||||
rules: NFTRule[];
|
||||
};
|
||||
/**
|
||||
* Render a human friendly one-line representation of a rule.
|
||||
* Accepts RuleOut (typed) but remains defensive against unexpected shapes.
|
||||
*/
|
||||
function renderRuleFriendly(rule: RuleOut | any): string {
|
||||
// prefer `text` if present
|
||||
if (rule?.text && typeof rule.text === 'string' && rule.text.trim() !== '') return rule.text;
|
||||
|
||||
type NFTTable = {
|
||||
family?: string | null;
|
||||
name: string;
|
||||
chains: NFTChain[];
|
||||
};
|
||||
// if expr is a string, show it
|
||||
if (rule?.expr && typeof rule.expr === 'string') return rule.expr;
|
||||
|
||||
/* Extract ruleset -> tables (unchanged) */
|
||||
function extractTablesFromParsed(parsed: any): NFTTable[] {
|
||||
if (!parsed) return [];
|
||||
|
||||
if (Array.isArray(parsed.nftables)) {
|
||||
const tablesMap = new Map<string, NFTTable>();
|
||||
const chainsMap = new Map<string, NFTChain>();
|
||||
|
||||
for (const item of parsed.nftables) {
|
||||
if (item.table) {
|
||||
const t = item.table;
|
||||
const key = `${t.family ?? 'n/a'}:${t.name}`;
|
||||
if (!tablesMap.has(key)) {
|
||||
tablesMap.set(key, { family: t.family ?? null, name: t.name, chains: [] });
|
||||
}
|
||||
} else if (item.chain) {
|
||||
const c = item.chain;
|
||||
const fam = c.family ?? 'n/a';
|
||||
const table = c.table ?? 'n/a';
|
||||
const tableKey = `${fam}:${table}`;
|
||||
|
||||
if (!tablesMap.has(tableKey)) {
|
||||
tablesMap.set(tableKey, { family: c.family ?? null, name: table, chains: [] });
|
||||
}
|
||||
|
||||
const chainKey = `${fam}:${table}:${c.name}`;
|
||||
const chainObj: NFTChain = {
|
||||
name: c.name,
|
||||
type: c.type ?? null,
|
||||
hook: c.hook ?? null,
|
||||
priority: (c.priority as number) ?? null,
|
||||
policy: c.policy ?? null,
|
||||
rules: [],
|
||||
};
|
||||
|
||||
chainsMap.set(chainKey, chainObj);
|
||||
tablesMap.get(tableKey)!.chains.push(chainObj);
|
||||
} else if (item.rule) {
|
||||
const r = item.rule;
|
||||
const fam = r.family ?? r.table?.family ?? 'n/a';
|
||||
const table = r.table ?? r.table?.name ?? r.table_name ?? 'n/a';
|
||||
const chainName = r.chain ?? r.chain?.name ?? 'unknown';
|
||||
|
||||
const tableKey = `${fam}:${table}`;
|
||||
const chainKey = `${fam}:${table}:${chainName}`;
|
||||
|
||||
if (!tablesMap.has(tableKey)) {
|
||||
tablesMap.set(tableKey, { family: fam ?? null, name: table, chains: [] });
|
||||
}
|
||||
|
||||
if (!chainsMap.has(chainKey)) {
|
||||
const newChain: NFTChain = { name: chainName, rules: [] };
|
||||
chainsMap.set(chainKey, newChain);
|
||||
tablesMap.get(tableKey)!.chains.push(newChain);
|
||||
}
|
||||
|
||||
tablesMap
|
||||
.get(tableKey)!
|
||||
.chains.find((c) => c.name === chainName)!
|
||||
.rules.push({ ...r });
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(tablesMap.values());
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed.tables)) {
|
||||
return parsed.tables.map((t: any) => ({
|
||||
family: t.family ?? null,
|
||||
name: t.name,
|
||||
chains: t.chains ?? [],
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Friendly rule renderer (keeps previous logic) */
|
||||
function renderRuleFriendly(rule: NFTRule): string {
|
||||
if (rule.text && typeof rule.text === 'string' && rule.text.trim() !== '') return rule.text;
|
||||
if (typeof rule.rule === 'string') return rule.rule;
|
||||
if (rule.expr && typeof rule.expr === 'string') return rule.expr;
|
||||
|
||||
const expr = rule.expr ?? rule;
|
||||
// if expr is an array, try to pretty print common tokens
|
||||
const expr = rule?.expr ?? rule;
|
||||
if (Array.isArray(expr)) {
|
||||
const tokens: string[] = [];
|
||||
for (const part of expr) {
|
||||
@@ -200,7 +107,8 @@ function renderRuleFriendly(rule: NFTRule): string {
|
||||
if (tokens.length > 0) return tokens.join(' ');
|
||||
}
|
||||
|
||||
if (rule.expr && typeof rule.expr === 'object') {
|
||||
// fallback: if expr is an object, stringify a short excerpt
|
||||
if (rule?.expr && typeof rule.expr === 'object') {
|
||||
try {
|
||||
return JSON.stringify(rule.expr, (_k, v) => (v === undefined ? null : v)).slice(0, 500);
|
||||
} catch {
|
||||
@@ -208,6 +116,7 @@ function renderRuleFriendly(rule: NFTRule): string {
|
||||
}
|
||||
}
|
||||
|
||||
// final fallback: stringify whole rule
|
||||
try {
|
||||
return JSON.stringify(rule, null, 2);
|
||||
} catch {
|
||||
@@ -221,36 +130,24 @@ function isSuccessRc(out?: ExecResult | null): boolean {
|
||||
return out.rc === 0 || out.rc === -1;
|
||||
}
|
||||
|
||||
/* Component */
|
||||
export default function FirewallTables(): ReactElement {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tables, setTables] = useState<NFTTable[]>([]);
|
||||
const [results, setResults] = useState<CmdResult[]>([]);
|
||||
/* ---------- Props ---------- */
|
||||
type Props = {
|
||||
tables: TableOut[]; // passed from parent
|
||||
error?: Error | null;
|
||||
refreshRules: () => Promise<void>; // trigger to re-fetch ruleset
|
||||
};
|
||||
|
||||
/* ---------- Component ---------- */
|
||||
|
||||
export default function FirewallTables({ tables, error, refreshRules: refresh }: Props): ReactElement {
|
||||
// local UI state, non-persistent
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [results, setResults] = useState<CmdResult[]>([]);
|
||||
const [isOpenTableCreatorModal, setIsOpenTableCreatorModal] = useState(false);
|
||||
const [isOpenChainCreatorModal, setIsOpenChainCreatorModal] = useState(false);
|
||||
|
||||
async function loadRuleset() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchRuleset();
|
||||
const parsed = extractTablesFromParsed((res as any).ruleset);
|
||||
setTables(parsed);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadRuleset();
|
||||
}, []);
|
||||
|
||||
async function runCommands(cmds: string[]) {
|
||||
// run raw nft commands sequentially and collect results (used for delete ops etc.)
|
||||
const runCommands = useCallback(async (cmds: string[]) => {
|
||||
const acc: CmdResult[] = [];
|
||||
for (const cmd of cmds) {
|
||||
try {
|
||||
@@ -261,230 +158,122 @@ export default function FirewallTables(): ReactElement {
|
||||
acc.push({ cmd, err: err?.message ?? String(err) });
|
||||
}
|
||||
}
|
||||
// prepend new results to history
|
||||
setResults((prev) => [...acc, ...prev]);
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Delete helpers (unchanged behavior)
|
||||
async function handleDeleteRule(
|
||||
family: string | null | undefined,
|
||||
table: string,
|
||||
chain: string,
|
||||
handle: number | string,
|
||||
) {
|
||||
const cmd = `delete rule ${family ?? 'inet'} ${table} ${chain} handle ${handle}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Rule',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>Are you sure you want to delete this rule?</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success('Rule deleted');
|
||||
else message.error('Delete returned error — check results panel');
|
||||
} catch (err: any) {
|
||||
message.error('Delete failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await loadRuleset();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// Delete rule
|
||||
const handleDeleteRule = useCallback(
|
||||
async (family: string | null | undefined, table: string, chain: string, handle: number | string) => {
|
||||
const cmd = `delete rule ${family ?? 'inet'} ${table} ${chain} handle ${handle}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Rule',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>Are you sure you want to delete this rule?</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success('Rule deleted');
|
||||
else message.error('Delete returned error — check results panel');
|
||||
} catch (err: any) {
|
||||
message.error('Delete failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[runCommands, refresh],
|
||||
);
|
||||
|
||||
async function handleDeleteChain(family: string | null | undefined, table: string, chain: string) {
|
||||
const cmd = `delete chain ${family ?? 'inet'} ${table} ${chain}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Chain',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>
|
||||
This will delete the chain <i>{chain}</i> in table <i>{table}</i> unrevertably.
|
||||
</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success(`Chain ${chain} deleted`);
|
||||
else message.error(`Chain deletion returned error — check results panel`);
|
||||
} catch (err: any) {
|
||||
message.error('Chain deletion failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await loadRuleset();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// Delete chain
|
||||
const handleDeleteChain = useCallback(
|
||||
async (family: string | null | undefined, table: string, chain: string) => {
|
||||
const cmd = `delete chain ${family ?? 'inet'} ${table} ${chain}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Chain',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>
|
||||
This will delete the chain <i>{chain}</i> in table <i>{table}</i> unrevertably.
|
||||
</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success(`Chain ${chain} deleted`);
|
||||
else message.error(`Chain deletion returned error — check results panel`);
|
||||
} catch (err: any) {
|
||||
message.error('Chain deletion failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[runCommands, refresh],
|
||||
);
|
||||
|
||||
async function handleDeleteTable(family: string | null | undefined, table: string) {
|
||||
const cmd = `delete table ${family ?? 'inet'} ${table}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Table',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>
|
||||
This will delete the table <i>{table}</i> including all its chains and rules unrevertably.
|
||||
</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success(`Table ${table} deleted`);
|
||||
else message.error(`Table deletion returned error — check results panel`);
|
||||
} catch (err: any) {
|
||||
message.error('Table deletion failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await loadRuleset();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// Delete table
|
||||
const handleDeleteTable = useCallback(
|
||||
async (family: string | null | undefined, table: string) => {
|
||||
const cmd = `delete table ${family ?? 'inet'} ${table}`;
|
||||
Modal.confirm({
|
||||
title: 'Delete Table',
|
||||
content: (
|
||||
<>
|
||||
<Paragraph>
|
||||
This will delete the table <i>{table}</i> including all its chains and rules unrevertably.
|
||||
</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph copyable>{cmd}</Paragraph>
|
||||
</>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await runCommands([cmd]);
|
||||
const first = res[0];
|
||||
if (!first.err) message.success(`Table ${table} deleted`);
|
||||
else message.error(`Table deletion returned error — check results panel`);
|
||||
} catch (err: any) {
|
||||
message.error('Table deletion failed: ' + (err?.message ?? String(err)));
|
||||
} finally {
|
||||
await refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[runCommands, refresh],
|
||||
);
|
||||
|
||||
async function handleRefresh() {
|
||||
// manual refresh trigger (exposed to UI)
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadRuleset();
|
||||
await refresh();
|
||||
message.success('Ruleset refreshed');
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}, [refresh]);
|
||||
|
||||
// Move using position (0-based). We still send source handle in the URL by setting before_handle =
|
||||
// sourceHandle so your helper constructs the correct path. The body contains "position".
|
||||
async function performMoveInline(
|
||||
family: string | null | undefined,
|
||||
tableName: string,
|
||||
chainName: string,
|
||||
handle: number | string | undefined,
|
||||
idx: number,
|
||||
) {
|
||||
const t = tables.find((x) => x.family === family && x.name === tableName);
|
||||
if (!t) {
|
||||
message.error('Table not found in local state');
|
||||
return;
|
||||
}
|
||||
const ch = t.chains.find((c) => c.name === chainName);
|
||||
if (!ch) {
|
||||
message.error('Chain not found in local state');
|
||||
return;
|
||||
}
|
||||
const tablesToRender = useMemo(() => tables ?? [], [tables]);
|
||||
|
||||
const lastIndex = ch.rules.length - 1;
|
||||
|
||||
// if no handle -> disallow
|
||||
if (handle === undefined || handle === null) {
|
||||
message.error('Rule has no handle; cannot perform server-side move.');
|
||||
return;
|
||||
}
|
||||
|
||||
// compute target positions for up/down actions invoked from UI; we assume caller computed newPos already
|
||||
// but here we'll ask the user which direction via a small confirm for safety.
|
||||
const confirm = await new Promise<boolean>((resolve) => {
|
||||
Modal.confirm({
|
||||
title: 'Move rule',
|
||||
content: `Move rule #${handle} (position ${idx + 1} of ${lastIndex + 1})?`,
|
||||
okText: 'Move up one',
|
||||
cancelText: 'Move down one',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
|
||||
// But above confirm uses ok/cancel as a quick choice. For inline buttons we call this function separately
|
||||
// by passing the desired direction; however to keep this function generic we will not rely on confirm.
|
||||
// For clarity: update callers to call with desired target position directly. (We handle here both.)
|
||||
return;
|
||||
}
|
||||
|
||||
// Inline-specific wrappers: moveUp / moveDown (these compute position and call moveRule)
|
||||
async function moveUp(family: string, tableName: string, chainName: string, handle: number | string, idx: number) {
|
||||
if (idx <= 0) {
|
||||
message.warning('Already at top');
|
||||
return;
|
||||
}
|
||||
// target position: idx - 1
|
||||
const targetPos = idx - 1;
|
||||
await callMoveApi(family, tableName, chainName, handle, targetPos);
|
||||
}
|
||||
|
||||
async function moveDown(family: string, tableName: string, chainName: string, handle: number | string, idx: number) {
|
||||
const t = tables.find((x) => x.family === family && x.name === tableName);
|
||||
if (!t) return;
|
||||
const ch = t.chains.find((c) => c.name === chainName);
|
||||
if (!ch) return;
|
||||
const lastIndex = ch.rules.length - 1;
|
||||
if (idx >= lastIndex) {
|
||||
message.warning('Already at bottom');
|
||||
return;
|
||||
}
|
||||
// target position: idx + 1
|
||||
const targetPos = idx + 1;
|
||||
await callMoveApi(family, tableName, chainName, handle, targetPos);
|
||||
}
|
||||
|
||||
// call your moveRule helper; we must supply before_handle as the source handle so the helper puts that in the URL
|
||||
// while the server will use 'position' from the body to place the rule.
|
||||
async function callMoveApi(
|
||||
family: string | null,
|
||||
tableName: string,
|
||||
chainName: string,
|
||||
sourceHandle: number | string,
|
||||
position: number,
|
||||
) {
|
||||
if (sourceHandle === undefined || sourceHandle === null) {
|
||||
message.error('Cannot move rule without a source handle.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reqBody = {
|
||||
family: family,
|
||||
table: tableName,
|
||||
chain: chainName,
|
||||
position: Number(position),
|
||||
before_handle: Number(sourceHandle),
|
||||
} as any;
|
||||
|
||||
message.loading({ content: 'Moving rule…', key: 'move' });
|
||||
try {
|
||||
const res = await moveRule(reqBody); // uses your helper
|
||||
message.success({ content: 'Rule moved', key: 'move' });
|
||||
} catch (err: any) {
|
||||
const detail = err?.response?.data ?? err?.message ?? String(err);
|
||||
message.error({
|
||||
content: 'Move failed: ' + (typeof detail === 'string' ? detail : JSON.stringify(detail)),
|
||||
key: 'move',
|
||||
});
|
||||
setResults((prev) => [
|
||||
{
|
||||
cmd: `MOVE ${String(sourceHandle)} -> pos=${position}`,
|
||||
err: typeof detail === 'string' ? detail : JSON.stringify(detail),
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
} finally {
|
||||
await loadRuleset();
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <Spin size="large" style={{ display: 'block', margin: '40px auto' }} />;
|
||||
|
||||
if (error) return <Alert type="error" message="Failed to load firewall rules" description={error} />;
|
||||
if (error)
|
||||
return <Alert type="error" message="Failed to load firewall rules" description={error.message ?? String(error)} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -500,16 +289,17 @@ export default function FirewallTables(): ReactElement {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{tables.length === 0 && (
|
||||
{tablesToRender.length === 0 && (
|
||||
<Alert
|
||||
type="info"
|
||||
title="No firewall tables found"
|
||||
message="No firewall tables found"
|
||||
description="You can create a new table using the button above."
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{tables.map((table) => {
|
||||
const totalRules = table.chains.reduce((acc, c) => acc + c.rules.length, 0);
|
||||
|
||||
{tablesToRender.map((table) => {
|
||||
const totalRules = table.chains.reduce((acc, c) => acc + (c.rules?.length ?? 0), 0);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -550,65 +340,40 @@ export default function FirewallTables(): ReactElement {
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{table.chains.map((chain) => {
|
||||
const lastIndex = chain.rules.length - 1;
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: 'Rule #', dataIndex: 'idx', width: 80 },
|
||||
{
|
||||
title: 'Handle',
|
||||
dataIndex: 'handle',
|
||||
width: 120,
|
||||
width: 80,
|
||||
render: (v) => v ?? '-',
|
||||
},
|
||||
{
|
||||
title: 'Rule',
|
||||
dataIndex: 'raw',
|
||||
render: (v) => (
|
||||
<Paragraph ellipsis={{ rows: 2, expandable: false }} copyable>
|
||||
{v}
|
||||
</Paragraph>
|
||||
),
|
||||
render: (v) => <Paragraph ellipsis={{ rows: 2, expandable: false }}>{v}</Paragraph>,
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
dataIndex: 'actions',
|
||||
width: 160,
|
||||
width: 80,
|
||||
render: (_: any, rec: any) =>
|
||||
rec.handle ? (
|
||||
<Space>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteRule(table.family, table.name, chain.name, rec.handle)}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ArrowUpOutlined />}
|
||||
disabled={rec.idx <= 1 || rec.handle === undefined || rec.handle === null}
|
||||
onClick={() =>
|
||||
moveUp(table.family ?? '', table.name, chain.name, rec.handle, rec.idx - 1)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ArrowDownOutlined />}
|
||||
disabled={rec.idx - 1 >= lastIndex || rec.handle === undefined || rec.handle === null}
|
||||
onClick={() =>
|
||||
moveDown(table.family ?? '', table.name, chain.name, rec.handle, rec.idx - 1)
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteRule(table.family, table.name, chain.name, rec.handle)}
|
||||
/>
|
||||
) : (
|
||||
<Space>
|
||||
<Button size="small" disabled icon={<DeleteOutlined />} />
|
||||
<Button size="small" disabled icon={<ArrowUpOutlined />} />
|
||||
<Button size="small" disabled icon={<ArrowDownOutlined />} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const dataSource = chain.rules.map((r, idx) => ({
|
||||
const dataSource = (chain.rules ?? []).map((r: RuleOut, idx: number) => ({
|
||||
key: `${chain.name}:${idx}`,
|
||||
idx: idx + 1,
|
||||
handle: r.handle ?? null,
|
||||
|
||||
Reference in New Issue
Block a user