restructure frontend and add network settings
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m41s

This commit is contained in:
2025-12-18 20:38:27 +01:00
parent b0de61fd9d
commit 9afe49857a
15 changed files with 1479 additions and 696 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 NetworkManager() {
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 direction="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) => s ?? '—' },
{
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 ?? [];
try {
await createBridge({ name: values.name, interfaces: ifaceList });
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');
}
}
async function handleRemoveBridge(name: string) {
try {
await removeBridge({ name });
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={4}>Bridges & Interfaces</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>
);
}

View File

@@ -1,10 +1,31 @@
import { Button, Typography } from 'antd';
import { useEffect } from 'react';
import { fetchFullState } from '../api/apiClient';
import { useNetwork } from '../hooks/useNetwork';
export default function Home() {
const { fullStateQuery } = useNetwork();
const { data, isLoading, isError, error } = fullStateQuery;
if (isLoading) return <div>Loading full state…</div>;
if (isError) return <div>Error: {(error as Error)?.message}</div>;
useEffect(() => {
fetchFullState().catch(() => {});
}, []);
return (
<>
<Typography>🚀 Hello MUI + React</Typography>
<Button color="primary">Klick mich!</Button>
</>
<div>
<h3>Full State</h3>
<div>
<strong>Interfaces:</strong> {data?.interfaces.length ?? 0}
</div>
<div>
<strong>Routes:</strong> {data?.routes.length ?? 0}
</div>
<div>
<strong>Bridges:</strong> {data?.bridges.length ?? 0}
</div>
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}