import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { Button, Card, Col, Descriptions, Form, Input, InputNumber, 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 { createBridge, disableBridgeLinkStateWatcher, enableBridgeLinkStateWatcher, fetchBridgeLinkStateWatchers, fetchFullState, removeBridge, } from '../api/apiClient'; import type { BridgeInfo, BridgeLinkStateWatcherStatus, FullState, InterfaceInfo, } from '../types/network'; const { Title, Paragraph } = Typography; export default function Network() { const [bridgeModalVisible, setBridgeModalVisible] = useState(false); const [watcherModalVisible, setWatcherModalVisible] = useState(false); const [bridgeForm] = Form.useForm(); const [watcherForm] = Form.useForm(); const [networkState, setNetworkState] = useState(); const [watcherStatuses, setWatcherStatuses] = useState>({}); const [watcherBridgeName, setWatcherBridgeName] = useState(); const [watcherBusyBridge, setWatcherBusyBridge] = useState(); const getNetworkPageState = (silent = false) => { Promise.all([fetchFullState(), fetchBridgeLinkStateWatchers()]) .then(([state, watchers]) => { setNetworkState(state); setWatcherStatuses( watchers.reduce>((acc, watcher) => { acc[watcher.bridge] = watcher; return acc; }, {}), ); if (!silent) { notification.success({ message: 'Success', description: 'Network state updated.', }); } }) .catch((error) => { console.error('Failed to fetch network state:', error); notification.error({ message: 'Error', description: 'Failed to fetch network state.', }); }); }; useEffect(() => { getNetworkPageState(true); }, []); const hasActiveWatchers = useMemo( () => Object.values(watcherStatuses).some((watcher) => watcher.active), [watcherStatuses], ); useEffect(() => { if (!hasActiveWatchers) { return undefined; } const intervalId = window.setInterval(() => { getNetworkPageState(true); }, 2000); return () => window.clearInterval(intervalId); }, [hasActiveWatchers]); const interfaceOptions = useMemo( () => networkState?.interfaces.map((iface: InterfaceInfo) => ({ label: iface.name, value: iface.name, })) ?? [], [networkState], ); const watcherData = useMemo( () => (networkState?.bridges ?? []).map((bridge) => ({ bridge, watcher: watcherStatuses[bridge.ifname], })), [networkState, watcherStatuses], ); const watcherMemberColumns = useMemo( () => [ { title: 'Member', dataIndex: 'ifname', key: 'ifname' }, { title: 'Admin', dataIndex: 'admin_up', key: 'admin_up', width: 90, render: (value: boolean | null | undefined) => {value ? 'up' : 'down'}, }, { title: 'Carrier', dataIndex: 'carrier_up', key: 'carrier_up', width: 100, render: (value: boolean | null | undefined) => {value ? 'up' : 'down'}, }, { title: 'Operstate', dataIndex: 'operstate', key: 'operstate', render: (value: string | null | undefined) => {value ?? 'unknown'}, }, { title: 'Role', key: 'role', width: 120, render: (_: unknown, record: { link_ready: boolean; suppressed: boolean }) => { if (record.suppressed) { return suppressed; } return {record.link_ready ? 'ready' : 'failing'}; }, }, ], [], ); const interfaceColumns: ColumnsType = useMemo( () => [ { title: 'IfIndex', dataIndex: 'ifindex', key: 'ifindex', width: 90 }, { title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'State', dataIndex: 'state', key: 'state', render: (state) => {state} }, { title: 'MAC', dataIndex: 'mac', key: 'mac', render: (mac) => mac ?? '—' }, { title: 'MTU', dataIndex: 'mtu', key: 'mtu', width: 90 }, { title: 'Addresses', dataIndex: 'addresses', key: 'addresses', render: (addresses: any[]) => addresses?.length ? ( {addresses.map((address) => ( {address.address}/{address.prefixlen} ({address.family}) ))} ) : ( — ), }, ], [], ); const bridgeColumns: ColumnsType = useMemo( () => [ { title: 'IfIndex', dataIndex: 'ifindex', key: 'ifindex', width: 90 }, { title: 'Name', dataIndex: 'ifname', key: 'ifname' }, { title: 'State', dataIndex: 'state', key: 'state', render: (state) => {state ?? '—'} }, { title: 'Members', dataIndex: 'members', key: 'members', render: (members: any[]) => (members?.length ? members.map((member) => member.ifname).join(', ') : '—'), }, { title: 'Link Watcher', key: 'watcher', width: 160, render: (_, record: BridgeInfo) => { const watcher = watcherStatuses[record.ifname]; if (!watcher?.active) { return disabled; } if (watcher.last_error) { return error; } if (watcher.suppressed_members.length > 0) { return propagating; } return active; }, }, { title: 'Actions', key: 'actions', width: 260, render: (_, record: BridgeInfo) => ( {watcherStatuses[record.ifname]?.active ? ( ) : ( )} handleRemoveBridge(record.ifname)} okText="Remove" cancelText="Cancel" > row.ifindex} dataSource={networkState?.interfaces ?? []} columns={interfaceColumns} pagination={{ pageSize: 8 }} size="small" /> } type="primary" onClick={() => { getNetworkPageState(true); setBridgeModalVisible(true); }} /> } >
String(row.ifindex)} dataSource={networkState?.bridges ?? []} columns={bridgeColumns} pagination={{ pageSize: 6 }} size="small" /> Enable a watcher on a bridge to force the sibling bridge ports down when one member loses link. {(networkState?.bridges.length ?? 0) === 0 ? ( No bridges available. ) : ( watcherData.map(({ bridge, watcher }) => { const members = Object.values(watcher?.members ?? {}); return ( {bridge.ifname} {watcher?.active ? 'watching' : 'disabled'} {watcher?.last_error ? error : null} } extra={ watcher?.active ? ( ) : ( ) } > {bridge.members.map((member) => member.ifname).join(', ') || '—'} {watcher?.event_driven ? 'netlink events' : '—'} {watcher?.recovery_holdoff_seconds ? `${watcher.recovery_holdoff_seconds}s` : '—'} {watcher?.suppressed_members.length ? watcher.suppressed_members.join(', ') : 'none'} {watcher?.last_action ?? watcher?.message ?? '—'} {members.length ? (
row.ifname} dataSource={members} columns={watcherMemberColumns} pagination={false} size="small" /> ) : ( {watcher?.active ? 'Waiting for watcher state.' : 'Watcher is disabled.'} )} ); }) )} { setBridgeModalVisible(false); bridgeForm.resetFields(); }} onOk={() => bridgeForm.submit()} >