improver error handling
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
This commit is contained in:
234
frontend/src/pages/Network.tsx
Normal file
234
frontend/src/pages/Network.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
// 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 type { ColumnsType } from 'antd/es/table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNetwork } from '../hooks/useNetwork';
|
||||
import type { BridgeInfo, InterfaceInfo } from '../types/network';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
export default function Network() {
|
||||
const {
|
||||
interfacesQuery,
|
||||
bridgesQuery,
|
||||
createBridge,
|
||||
removeBridge,
|
||||
fetchBridges,
|
||||
fetchInterfaces,
|
||||
refreshBridges,
|
||||
refreshInterfaces,
|
||||
isCreating,
|
||||
isRemoving,
|
||||
} = useNetwork();
|
||||
|
||||
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
|
||||
const [bridgeForm] = Form.useForm();
|
||||
|
||||
// fetch on mount (explicit, since queries are disabled by default)
|
||||
useEffect(() => {
|
||||
fetchInterfaces().catch(() => {});
|
||||
fetchBridges().catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Table data
|
||||
const interfaces = interfacesQuery.data ?? [];
|
||||
const bridges = bridgesQuery.data ?? [];
|
||||
|
||||
// build select options from interfaces list
|
||||
const interfaceOptions = useMemo(
|
||||
() =>
|
||||
interfaces.map((it: InterfaceInfo) => ({
|
||||
label: it.name,
|
||||
value: it.name,
|
||||
})),
|
||||
[interfaces],
|
||||
);
|
||||
|
||||
// Columns for interfaces table (read-only)
|
||||
const interfaceColumns: ColumnsType<InterfaceInfo> = useMemo(
|
||||
() => [
|
||||
{ title: 'IfIndex', dataIndex: 'ifindex', key: 'ifindex', width: 90 },
|
||||
{ title: 'Name', dataIndex: 'name', key: 'name' },
|
||||
{ title: 'State', dataIndex: 'state', key: 'state', render: (s) => <Tag>{s}</Tag> },
|
||||
{ title: 'MAC', dataIndex: 'mac', key: 'mac', render: (m) => m ?? '—' },
|
||||
{ title: 'MTU', dataIndex: 'mtu', key: 'mtu', width: 90 },
|
||||
{
|
||||
title: 'Addresses',
|
||||
dataIndex: 'addresses',
|
||||
key: 'addresses',
|
||||
render: (addrs: any[]) =>
|
||||
addrs?.length ? (
|
||||
<Space orientation="vertical">
|
||||
{addrs.map((a) => (
|
||||
<span key={`${a.address}/${a.prefixlen}`}>
|
||||
{a.address}/{a.prefixlen} ({a.family})
|
||||
</span>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<span>—</span>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// Columns for bridges table (with remove action)
|
||||
const bridgeColumns: ColumnsType<BridgeInfo> = useMemo(
|
||||
() => [
|
||||
{ title: 'IfIndex', dataIndex: 'ifindex', key: 'ifindex', width: 90 },
|
||||
{ title: 'Name', dataIndex: 'ifname', key: 'ifname' },
|
||||
{ title: 'State', dataIndex: 'state', key: 'state', render: (s) => <Tag>{s ?? '—'}</Tag> },
|
||||
{
|
||||
title: 'Members',
|
||||
dataIndex: 'members',
|
||||
key: 'members',
|
||||
render: (members: any[]) => (members?.length ? members.map((m) => m.ifname).join(', ') : '—'),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
render: (_: any, record: BridgeInfo) => (
|
||||
<Popconfirm
|
||||
title={`Remove bridge ${record.ifname}?`}
|
||||
onConfirm={() => handleRemoveBridge(record.ifname)}
|
||||
okText="Remove"
|
||||
cancelText="Cancel"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} loading={isRemoving} size="small">
|
||||
Remove
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
[isRemoving],
|
||||
);
|
||||
async function handleCreateBridge(values: { name: string; interfaces?: string[] }) {
|
||||
const ifaceList = values.interfaces ?? [];
|
||||
createBridge({ name: values.name, interfaces: ifaceList })
|
||||
.then(() => {
|
||||
message.success(`Bridge ${values.name} created`);
|
||||
setBridgeModalVisible(false);
|
||||
bridgeForm.resetFields();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
message.error((err as Error).message ?? 'Failed to create bridge');
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveBridge(name: string) {
|
||||
removeBridge({ name })
|
||||
.then(() => {
|
||||
message.success(`Bridge ${name} removed`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
message.error((err as Error).message ?? 'Failed to remove bridge');
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
|
||||
<Col>
|
||||
<Title level={2}> Network Management</Title>
|
||||
<Paragraph type="secondary">View system interfaces and manage network bridges.</Paragraph>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
// ensure up-to-date interface list when opening modal
|
||||
refreshInterfaces();
|
||||
setBridgeModalVisible(true);
|
||||
}}
|
||||
>
|
||||
Create bridge
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
refreshBridges();
|
||||
refreshInterfaces();
|
||||
message.success('Refreshing…');
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
<div style={{ background: '#fff', padding: 12, borderRadius: 6, minHeight: 220 }}>
|
||||
<Title level={5} style={{ marginBottom: 12 }}>
|
||||
Interfaces ({interfaces.length})
|
||||
</Title>
|
||||
<Table
|
||||
rowKey={(r: InterfaceInfo) => r.ifindex}
|
||||
dataSource={interfaces}
|
||||
columns={interfaceColumns}
|
||||
pagination={{ pageSize: 8 }}
|
||||
size="small"
|
||||
loading={interfacesQuery.isFetching || interfacesQuery.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col span={10}>
|
||||
<div style={{ background: '#fff', padding: 12, borderRadius: 6, minHeight: 220 }}>
|
||||
<Title level={5} style={{ marginBottom: 12 }}>
|
||||
Bridges ({bridges.length})
|
||||
</Title>
|
||||
<Table
|
||||
rowKey={(r: BridgeInfo) => String(r.ifindex)}
|
||||
dataSource={bridges}
|
||||
columns={bridgeColumns}
|
||||
pagination={{ pageSize: 6 }}
|
||||
size="small"
|
||||
loading={bridgesQuery.isFetching || bridgesQuery.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Create Bridge Modal */}
|
||||
<Modal
|
||||
title="Create Bridge"
|
||||
open={bridgeModalVisible}
|
||||
onCancel={() => {
|
||||
setBridgeModalVisible(false);
|
||||
bridgeForm.resetFields();
|
||||
}}
|
||||
okButtonProps={{ loading: isCreating }}
|
||||
onOk={() => bridgeForm.submit()}
|
||||
>
|
||||
<Form form={bridgeForm} layout="vertical" onFinish={handleCreateBridge}>
|
||||
<Form.Item name="name" label="Bridge name" rules={[{ required: true }]}>
|
||||
<Input placeholder="e.g. br0" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Select field populated from interfaces endpoint */}
|
||||
<Form.Item name="interfaces" label="Interfaces (select one or more)">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={interfaces.length ? 'Select interfaces...' : 'No interfaces available'}
|
||||
options={interfaceOptions}
|
||||
showSearch
|
||||
allowClear
|
||||
loading={interfacesQuery.isFetching || interfacesQuery.isLoading}
|
||||
disabled={interfacesQuery.isFetching || interfacesQuery.isLoading || interfaceOptions.length === 0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user