diff --git a/frontend/src/Layout/Sidebar.tsx b/frontend/src/Layout/Sidebar.tsx index 5a773d1..6d165bf 100644 --- a/frontend/src/Layout/Sidebar.tsx +++ b/frontend/src/Layout/Sidebar.tsx @@ -1,8 +1,15 @@ // src/components/Sidebar.tsx -import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons'; +import { + ApartmentOutlined, + HomeOutlined, + InfoCircleOutlined, + MonitorOutlined, + SettingOutlined, +} from '@ant-design/icons'; import { Layout, Menu } from 'antd'; import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import FirewallIcon from '../icons/FirewallIcon'; import TerminalIcon from '../icons/TerminalIcon'; import { PATHS } from '../routes'; import '../theme/layout.less'; @@ -12,13 +19,13 @@ const { Sider } = Layout; const menuItems = [ { key: PATHS.HOME, icon: , label: 'Home' }, { key: PATHS.NETWORK, icon: , label: 'Network' }, - { key: PATHS.SNIFFING, icon: , label: 'Sniffing' }, + { key: PATHS.SNIFFING, icon: , label: 'Sniffing' }, { key: PATHS.SCRIPTING, icon: , label: 'Scripting', }, - { key: PATHS.FIREWALL, icon: , label: 'Firewall' }, + { key: PATHS.FIREWALL, icon: , label: 'Firewall' }, { key: '/about', icon: , label: 'About' }, { key: '/settings', icon: , label: 'Settings' }, ]; diff --git a/frontend/src/components/FirewallRuleBuilder.tsx b/frontend/src/components/FirewallRuleBuilder.tsx index bc10ae1..901987b 100644 --- a/frontend/src/components/FirewallRuleBuilder.tsx +++ b/frontend/src/components/FirewallRuleBuilder.tsx @@ -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 = (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 = (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 = (props: RuleBuilderProps) }; return ( - - - - - Add Firewall Rule (raw) - - - Build an nft rule using canonical match list (meta/ct/ip/ip6/tcp/udp/icmp/ether/vlan). - - - - - - - - - - - - +
= (props: RuleBuilderProps)
- nft command preview: + nft command preview:
- + {cmdPreview || '(no textual preview available)'}
- -
diff --git a/frontend/src/components/FirewallRulesetViewer.tsx b/frontend/src/components/FirewallRulesetViewer.tsx index 5254048..96fcebc 100644 --- a/frontend/src/components/FirewallRulesetViewer.tsx +++ b/frontend/src/components/FirewallRulesetViewer.tsx @@ -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([]); 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 }: ); })} - - {/* Results panel */} - {results.length > 0 && ( - - {results.map((r, i) => ( -
- {r.cmd} - {r.err ? ( - {r.err} - ) : r.out ? ( - <> - - rc: {r.out.rc} - - {r.out.stdout ? ( - <> - stdout: -
{r.out.stdout}
- - ) : null} - {r.out.stderr ? ( - <> - stderr: -
{r.out.stderr}
- - ) : null} - - ) : null} -
- ))} -
- )} ); } diff --git a/frontend/src/components/PacketViewer.tsx b/frontend/src/components/PacketViewer.tsx index 356d601..f38f8c4 100644 --- a/frontend/src/components/PacketViewer.tsx +++ b/frontend/src/components/PacketViewer.tsx @@ -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 ( -
+
- - Packets - + Packets
Live packet viewer — history + live stream
diff --git a/frontend/src/components/SnifferManager.tsx b/frontend/src/components/SnifferManager.tsx index a0f90df..9a53b7a 100644 --- a/frontend/src/components/SnifferManager.tsx +++ b/frontend/src/components/SnifferManager.tsx @@ -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 { } }} diff --git a/frontend/src/icons/FirewallIcon.tsx b/frontend/src/icons/FirewallIcon.tsx new file mode 100644 index 0000000..8c950e8 --- /dev/null +++ b/frontend/src/icons/FirewallIcon.tsx @@ -0,0 +1,59 @@ +import React, { forwardRef } from 'react'; + +export type IconProps = React.SVGProps & { + /** + * Width/height of the icon. If a number is provided it will be used as px. + * Default: 24 + */ + size?: number | string; + /** + * Icon color — will be used as the fill for paths. + * Default: 'currentColor' so color can be controlled via CSS. + */ + color?: string; + /** + * Accessible title. If provided, title will be rendered and aria-hidden will be false. + */ + title?: string; +}; + +const FirewallIcon = forwardRef( + ({ size = 24, color = 'currentColor', title, ...rest }, ref) => { + // If user passed a numeric size, treat as px + const sizeValue = typeof size === 'number' ? `${size}px` : size; + + return ( + + + + + + + ); + }, +); + +FirewallIcon.displayName = 'FirewallIcon'; + +export default FirewallIcon; diff --git a/frontend/src/pages/Network.tsx b/frontend/src/pages/Network.tsx index e01eeb0..8a597e1 100644 --- a/frontend/src/pages/Network.tsx +++ b/frontend/src/pages/Network.tsx @@ -1,48 +1,67 @@ // src/pages/BridgesManager.tsx import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; -import { Button, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd'; +import { + Button, + Card, + Col, + Form, + Input, + Modal, + notification, + Popconfirm, + Row, + Select, + Space, + Table, + Tag, + Typography, +} from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { useEffect, useMemo, useState } from 'react'; -import { useBackendAPI } from '../hooks/useBackendAPI'; +import { createBridge, fetchFullState, removeBridge } from '../api/apiClient'; import type { BridgeInfo, InterfaceInfo } from '../types/network'; +import { FullState } from '../types/network'; const { Title, Paragraph } = Typography; export default function Network() { - const { - interfacesQuery, - bridgesQuery, - createBridge, - removeBridge, - fetchBridges, - fetchInterfaces, - refreshBridges, - refreshInterfaces, - isCreating, - isRemoving, - } = useBackendAPI(); - const [bridgeModalVisible, setBridgeModalVisible] = useState(false); const [bridgeForm] = Form.useForm(); + const [networkState, setNetworkState] = useState(); + + const getFullState = (auto: boolean = false) => { + fetchFullState() + .then((interfaces) => { + setNetworkState(interfaces); + if (!auto) { + notification.success({ + message: 'Success', + description: 'Network state updated.', + }); + } + }) + .catch((err) => { + console.error('Failed to fetch interfaces:', err); + notification.error({ + message: 'Error', + description: 'Failed to fetch network state.', + }); + }); + }; // fetch on mount (explicit, since queries are disabled by default) useEffect(() => { - fetchInterfaces().catch(() => {}); - fetchBridges().catch(() => {}); + getFullState(true); }, []); - // Table data - const interfaces = interfacesQuery.data ?? []; - const bridges = bridgesQuery.data ?? []; - // build select options from interfaces list const interfaceOptions = useMemo( () => - interfaces.map((it: InterfaceInfo) => ({ + networkState?.interfaces.map((it: InterfaceInfo) => ({ label: it.name, value: it.name, - })), - [interfaces], + })) ?? [], + [networkState], ); // Columns for interfaces table (read-only) @@ -97,37 +116,49 @@ export default function Network() { okText="Remove" cancelText="Cancel" > - +