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

@@ -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<FullState>();
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"
>
<Button danger icon={<DeleteOutlined />} loading={isRemoving} size="small">
Remove
</Button>
<Button danger icon={<DeleteOutlined />} />
</Popconfirm>
),
},
],
[isRemoving],
[networkState],
);
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`);
notification.success({
message: 'Success',
description: `Bridge ${values.name} created`,
});
setBridgeModalVisible(false);
getFullState(true);
bridgeForm.resetFields();
})
.catch((err) => {
console.error(err);
message.error((err as Error).message ?? 'Failed to create bridge');
notification.error({
message: 'Error',
description: (err as Error).message ?? 'Failed to create bridge',
});
});
}
function handleRemoveBridge(name: string) {
removeBridge({ name })
.then(() => {
message.success(`Bridge ${name} removed`);
notification.success({
message: 'Success',
description: `Bridge ${name} removed`,
});
getFullState(true);
})
.catch((err) => {
console.error(err);
message.error((err as Error).message ?? 'Failed to remove bridge');
notification.error({
message: 'Error',
description: (err as Error).message ?? 'Failed to remove bridge',
});
});
}
@@ -140,23 +171,10 @@ export default function Network() {
</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…');
getFullState();
}}
>
Refresh
@@ -167,35 +185,41 @@ export default function Network() {
<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>
<Card title={`Interfaces (${networkState?.interfaces.length})`} style={{ overflow: 'auto' }}>
<Table
rowKey={(r: InterfaceInfo) => r.ifindex}
dataSource={interfaces}
dataSource={networkState?.interfaces ?? []}
columns={interfaceColumns}
pagination={{ pageSize: 8 }}
size="small"
loading={interfacesQuery.isFetching || interfacesQuery.isLoading}
/>
</div>
</Card>
</Col>
<Col span={10}>
<div style={{ background: '#fff', padding: 12, borderRadius: 6, minHeight: 220 }}>
<Title level={5} style={{ marginBottom: 12 }}>
Bridges ({bridges.length})
</Title>
<Card
title={`Bridges (${networkState?.bridges.length})`}
style={{ overflow: 'auto' }}
extra={
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => {
// ensure up-to-date interface list when opening modal
getFullState(true);
setBridgeModalVisible(true);
}}
/>
}
>
<Table
rowKey={(r: BridgeInfo) => String(r.ifindex)}
dataSource={bridges}
dataSource={networkState?.bridges ?? []}
columns={bridgeColumns}
pagination={{ pageSize: 6 }}
size="small"
loading={bridgesQuery.isFetching || bridgesQuery.isLoading}
/>
</div>
</Card>
</Col>
</Row>
@@ -207,7 +231,6 @@ export default function Network() {
setBridgeModalVisible(false);
bridgeForm.resetFields();
}}
okButtonProps={{ loading: isCreating }}
onOk={() => bridgeForm.submit()}
>
<Form form={bridgeForm} layout="vertical" onFinish={handleCreateBridge}>
@@ -219,12 +242,11 @@ export default function Network() {
<Form.Item name="interfaces" label="Interfaces (select one or more)">
<Select
mode="multiple"
placeholder={interfaces.length ? 'Select interfaces...' : 'No interfaces available'}
placeholder={networkState?.interfaces.length ? 'Select interfaces...' : 'No interfaces available'}
options={interfaceOptions}
showSearch
allowClear
loading={interfacesQuery.isFetching || interfacesQuery.isLoading}
disabled={interfacesQuery.isFetching || interfacesQuery.isLoading || interfaceOptions.length === 0}
disabled={interfaceOptions.length === 0}
/>
</Form.Item>
</Form>