import { useState } from 'react' import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, Typography, message } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../../api/client' interface NetworkInterface { id: number name: string type: 'ethernet' | 'vlan' | 'bond' | 'bridge' | 'wireguard' parent?: string | null vlan_id?: number | null role: 'wan' | 'lan' | 'dmz' | 'mgmt' | 'cluster' mtu?: number | null active: boolean description?: string | null created_at: string updated_at: string } interface IfaceFormValues { name: string type: NetworkInterface['type'] parent?: string vlan_id?: number role: NetworkInterface['role'] mtu?: number active: boolean description?: string } interface SystemInterface { ifname: string link_type?: string address?: string flags?: string[] addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }> } async function listInterfaces(): Promise { const r = await apiClient.get('/network-interfaces') if (!isEnvelope(r.data)) return [] return (r.data.data as { interfaces?: NetworkInterface[] }).interfaces ?? [] } async function listSystemInterfaces(): Promise { const r = await apiClient.get('/system/interfaces') if (!isEnvelope(r.data)) return [] return (r.data.data as { interfaces?: SystemInterface[] }).interfaces ?? [] } export default function NetworksPage() { const { t } = useTranslation() const qc = useQueryClient() const { data: ifs, isLoading } = useQuery({ queryKey: ['network-interfaces'], queryFn: listInterfaces }) const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces, refetchInterval: 60_000 }) const [editing, setEditing] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const create = useMutation({ mutationFn: async (v: IfaceFormValues) => { await apiClient.post('/network-interfaces', v) }, onSuccess: () => { message.success(t('common.save')) setCreating(false); form.resetFields() void qc.invalidateQueries({ queryKey: ['network-interfaces'] }) }, }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: IfaceFormValues }) => { await apiClient.put(`/network-interfaces/${id}`, v) }, onSuccess: () => { message.success(t('common.save')) setEditing(null); form.resetFields() void qc.invalidateQueries({ queryKey: ['network-interfaces'] }) }, }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/network-interfaces/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['network-interfaces'] }) }, }) const roleColor: Record = { wan: 'blue', lan: 'green', dmz: 'orange', mgmt: 'purple', cluster: 'magenta', } const columns: ColumnsType = [ { title: t('networks.name'), dataIndex: 'name', key: 'name', render: (s: string) => {s} }, { title: t('networks.type'), dataIndex: 'type', key: 'type' }, { title: t('networks.vlan'), key: 'vlan', render: (_, row) => row.type === 'vlan' ? {row.parent}.{row.vlan_id} : '—', }, { title: t('networks.role'), dataIndex: 'role', key: 'role', render: (r: NetworkInterface['role']) => {r.toUpperCase()}, }, { title: t('networks.mtu'), dataIndex: 'mtu', key: 'mtu', render: (v?: number) => v ?? '—' }, { title: t('networks.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => v ? t('common.yes') : t('common.no') }, { title: t('networks.actions'), key: 'actions', render: (_, row) => ( del.mutate(row.id)} > ), }, ] return (
{t('networks.title')} {t('networks.intro')} {(sys ?? []).map((i) => { const v4 = (i.addr_info ?? []).filter((a) => a.family === 'inet').map((a) => `${a.local}/${a.prefixlen}`) const v6 = (i.addr_info ?? []).filter((a) => a.family === 'inet6').map((a) => `${a.local}/${a.prefixlen}`) return ( {i.ifname}{v4[0] ? ` · ${v4[0]}` : ''} ) })} {(sys ?? []).length === 0 && } { setEditing(null); setCreating(false) }} onOk={() => { void form.submit() }} confirmLoading={create.isPending || update.isPending} width={560} >
{ if (editing) update.mutate({ id: editing.id, v }) else create.mutate(v) }} > ) : null}