cleanup frontend notifications
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m39s

This commit is contained in:
2026-03-01 20:47:29 +01:00
parent 0f2a342b75
commit 7812f8715f
7 changed files with 224 additions and 162 deletions

View File

@@ -10,7 +10,7 @@
//
// NOTE: This file replaces the token lists and per-field UI to strictly follow the user's canonical list.
import { CopyOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
Card,
@@ -22,11 +22,10 @@ import {
InputNumber,
message,
Modal,
notification,
Radio,
Row,
Select,
Space,
Tooltip,
Typography,
} from 'antd';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -584,9 +583,14 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
const out: ExecResult = await execFirewallRaw(cmd);
const stderrText = out?.stderr ? String(out.stderr).trim() : '';
if (stderrText) {
message.error(`Command produced stderr: ${stderrText}`);
notification.error({
message: 'Command produced Error',
description: stderrText,
});
} else if (out && (out.rc === 0 || out.rc === -1)) {
message.success('Command executed (rc ' + out.rc + ')');
notification.success({
message: 'Command executed successfully',
});
if (props.refreshRules) await props.refreshRules();
if (props.onCreated) await props.onCreated();
} else {
@@ -595,10 +599,16 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
(out.stdout ? ` stdout:${out.stdout}` : '') +
(out.stderr ? ` stderr:${out.stderr}` : '')
: 'unknown result';
message.error('Command failed: ' + info);
notification.error({
message: 'Command failed',
description: info,
});
}
} catch (err: any) {
message.error('Execution failed: ' + (err?.message ?? String(err)));
notification.error({
message: 'Execution failed',
description: err?.message ?? String(err),
});
} finally {
setLoading(false);
}
@@ -670,28 +680,7 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
};
return (
<Card>
<Row justify="space-between" align="middle">
<Col>
<Title level={4} style={{ margin: 0 }}>
Add Firewall Rule (raw)
</Title>
<Text type="secondary">
Build an nft rule using canonical match list (meta/ct/ip/ip6/tcp/udp/icmp/ether/vlan).
</Text>
</Col>
<Col>
<Space>
<Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={refreshing}>
Refresh tables
</Button>
</Space>
</Col>
</Row>
<Divider />
<Card title="Firewall Rule Builder" extra>
<Form
layout="vertical"
form={form}
@@ -1703,29 +1692,17 @@ export const RuleBuilder: React.FC<RuleBuilderProps> = (props: RuleBuilderProps)
<Form.Item>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1 }}>
<Text strong>nft command preview:</Text>
<Title level={5}>nft command preview:</Title>
<div style={{ marginTop: 6 }}>
<Text code copyable={{ text: cmdPreview || '' }}>
<Text code copyable={{ text: cmdPreview || '' }} style={{ fontSize: '16px' }}>
{cmdPreview || '(no textual preview available)'}
</Text>
</div>
</div>
<div style={{ display: 'flex', gap: 8, minWidth: 320, justifyContent: 'flex-end' }}>
<Tooltip title="Copy command to clipboard">
<Button
icon={<CopyOutlined />}
size="small"
onClick={() => {
if (!cmdPreview) return;
navigator.clipboard?.writeText(cmdPreview);
message.success('Command copied');
}}
/>
</Tooltip>
<Button type="primary" htmlType="submit" loading={loading} icon={<PlusOutlined />} disabled={noTables}>
Run command
Add Rule
</Button>
</div>
</div>

View File

@@ -132,7 +132,6 @@ type Props = {
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);
@@ -148,8 +147,6 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
acc.push({ cmd, err: err?.message ?? String(err) });
}
}
// prepend new results to history
setResults((prev) => [...acc, ...prev]);
return acc;
}, []);
@@ -466,38 +463,6 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
);
})}
</Card>
{/* Results panel */}
{results.length > 0 && (
<Card title="Command Results" style={{ marginTop: 20 }}>
{results.map((r, i) => (
<div key={i} style={{ marginBottom: 12 }}>
<Text strong>{r.cmd}</Text>
{r.err ? (
<Paragraph style={{ color: 'var(--ant-error-color)', marginTop: 6 }}>{r.err}</Paragraph>
) : r.out ? (
<>
<Paragraph>
<Text type="secondary">rc:</Text> {r.out.rc}
</Paragraph>
{r.out.stdout ? (
<>
<Text type="secondary">stdout:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stdout}</pre>
</>
) : null}
{r.out.stderr ? (
<>
<Text type="secondary">stderr:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stderr}</pre>
</>
) : null}
</>
) : null}
</div>
))}
</Card>
)}
</>
);
}

View File

@@ -19,7 +19,7 @@ import {
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { clearPackets, fetchPackets } from '../api/apiClient';
const { Text } = Typography;
const { Text, Title } = Typography;
const { Option } = Select;
type PacketRow = {
@@ -409,12 +409,10 @@ export default function PacketViewer(): ReactElement {
);
return (
<div style={{ padding: 16 }}>
<div>
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
<Col>
<Text strong style={{ fontSize: 18 }}>
Packets
</Text>
<Title level={3}>Packets</Title>
<div>
<Text type="secondary">Live packet viewer — history + live stream</Text>
</div>

View File

@@ -7,7 +7,21 @@ import {
ReloadOutlined,
StopOutlined,
} from '@ant-design/icons';
import { Button, Card, Form, List, message, Modal, Radio, Select, Space, Spin, Tag, Tooltip, Typography } from 'antd';
import {
Button,
Card,
Form,
List,
Modal,
notification,
Radio,
Select,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd';
import { ReactElement, useMemo, useState } from 'react';
import { startSniffer, stopSniffer, stopSnifferByInterface } from '../api/apiClient';
import { BridgeInfo, InterfaceInfo } from '../types/network';
@@ -52,18 +66,21 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
const handleStartSubmit = async (values: any) => {
const { target } = values;
if (!target) {
message.warning('Please select a target to start sniffing on.');
notification.warning({ message: 'Warning', description: 'Please select a target to start sniffing on.' });
return;
}
try {
const payload = startMode === 'interface' ? { interface: target } : { bridge: target };
const res = await startSniffer(payload);
message.success(`Sniffer started on ${target} (session ${res.session_id})`);
notification.success({
message: 'Sniffer started',
description: `Sniffer started on ${target} (session ${res.session_id})`,
});
await props.refreshAll();
setIsModalOpen(false);
} catch (err: any) {
console.error('startSniffer error', err);
message.error(err?.message ?? 'Failed to start sniffer');
notification.error(err?.message ?? 'Failed to start sniffer');
} finally {
}
};
@@ -72,11 +89,13 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
const handleStopAll = async () => {
try {
await stopSniffer();
message.success('All sniffers stopped');
notification.success({
message: 'All sniffers stopped',
});
await props.refreshStatus();
} catch (err: any) {
console.error('stopSniffer error', err);
message.error(err?.message ?? 'Failed to stop sniffers');
notification.error(err?.message ?? 'Failed to stop sniffers');
} finally {
}
};
@@ -86,7 +105,10 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
try {
// prefer stop by interface
await stopSnifferByInterface(ifaceName);
message.success(`Stopped sniffing on ${ifaceName}`);
notification.success({
message: 'Sniffer stopped',
description: `Sniffer stopped on interface ${ifaceName}`,
});
await props.refreshStatus();
return;
} catch (err: any) {
@@ -95,14 +117,20 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
if (session_id) {
try {
await stopSniffer({ session_id });
message.success(`Stopped session ${session_id}`);
notification.success({
message: 'Sniffer stopped',
description: `Sniffer stopped on interface ${ifaceName} (session ${session_id})`,
});
await props.refreshStatus();
return;
} catch (err2: any) {
console.error('stopSniffer by session fallback failed', err2);
}
}
message.error(err?.message ?? 'Failed to stop sniffer');
notification.error({
message: 'Failed to stop sniffer',
description: err?.message ?? 'Failed to stop sniffer',
});
} finally {
}
};
@@ -169,11 +197,17 @@ export default function SnifferManager(props: SnifferManagerProps): ReactElement
// start on this interface
try {
const res = await startSniffer({ interface: name });
message.success(`Started sniffer on ${name} (session ${res.session_id})`);
notification.success({
message: 'Sniffer started',
description: `Sniffer started on ${name} (session ${res.session_id})`,
});
await props.refreshStatus();
} catch (err: any) {
console.error('startSniffer quick', err);
message.error(err?.message ?? 'Failed to start sniffer');
notification.error({
message: 'Failed to start sniffer',
description: err?.message ?? 'Failed to start sniffer',
});
} finally {
}
}}