improver error handling
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s

This commit is contained in:
2025-12-18 21:45:43 +01:00
parent 9afe49857a
commit a2a017f900
7 changed files with 52 additions and 38 deletions

View 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>
);
}