import { useState } from 'react' import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, 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 IPAddress { id: number interface_id: number address: string prefix: number is_vip: boolean vip_priority?: number | null description?: string | null active: boolean created_at: string updated_at: string } interface SystemInterface { ifname: string flags?: string[] link_type?: string addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }> } interface SystemAddress { ifname: string family: 'inet' | 'inet6' address: string prefix: number } interface IPFormValues { interface_id: number address: string prefix: number is_vip: boolean vip_priority?: number description?: string active: boolean } interface NetworkInterface { id: number; name: string; role: string } async function listIPs(): Promise { const r = await apiClient.get('/ip-addresses') if (!isEnvelope(r.data)) return [] return (r.data.data as { ip_addresses?: IPAddress[] }).ip_addresses ?? [] } async function listIfaces(): Promise { const r = await apiClient.get('/network-interfaces') if (!isEnvelope(r.data)) return [] return (r.data.data as { interfaces?: NetworkInterface[] }).interfaces ?? [] } // Flatten /system/interfaces into one row per (ifname, address) so // the operator sees every kernel-side IP at a glance — including // addresses that EdgeGuard hasn't taken under management yet. async function listSystemAddresses(): Promise { const r = await apiClient.get('/system/interfaces') if (!isEnvelope(r.data)) return [] const ifs = (r.data.data as { interfaces?: SystemInterface[] }).interfaces ?? [] const out: SystemAddress[] = [] for (const i of ifs) { for (const a of i.addr_info ?? []) { out.push({ ifname: i.ifname, family: a.family, address: a.local, prefix: a.prefixlen, }) } } return out } export default function IPAddressesPage() { const { t } = useTranslation() const qc = useQueryClient() const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs }) const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces }) const { data: sysAddrs } = useQuery({ queryKey: ['system', 'addresses'], queryFn: listSystemAddresses, refetchInterval: 60_000, }) const ifaceLabel = (id: number) => { const i = ifs?.find((x) => x.id === id) return i ? `${i.name} (${i.role})` : `#${id}` } const [editing, setEditing] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const create = useMutation({ mutationFn: async (v: IPFormValues) => { await apiClient.post('/ip-addresses', v) }, onSuccess: () => { message.success(t('common.save')) setCreating(false); form.resetFields() void qc.invalidateQueries({ queryKey: ['ip-addresses'] }) }, }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: IPFormValues }) => { await apiClient.put(`/ip-addresses/${id}`, v) }, onSuccess: () => { message.success(t('common.save')) setEditing(null); form.resetFields() void qc.invalidateQueries({ queryKey: ['ip-addresses'] }) }, }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/ip-addresses/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['ip-addresses'] }) }, }) const columns: ColumnsType = [ { title: t('ips.interface'), dataIndex: 'interface_id', key: 'iface', render: (id: number) => ifaceLabel(id) }, { title: t('ips.address'), key: 'addr', render: (_, row) => {row.address}/{row.prefix}, }, { title: t('ips.vip'), key: 'vip', render: (_, row) => row.is_vip ? VIP{row.vip_priority != null ? ` · prio ${row.vip_priority}` : ''} : '—', }, { title: t('ips.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => v ? t('common.yes') : t('common.no') }, { title: t('ips.description'), dataIndex: 'description', key: 'desc', render: (v?: string) => v ?? '—' }, { title: t('ips.actions'), key: 'actions', render: (_, row) => ( del.mutate(row.id)} > ), }, ] return (
{t('ips.title')} {t('ips.intro')} {(sysAddrs ?? []).length === 0 ? : ( `${r.ifname}-${r.address}`} dataSource={sysAddrs ?? []} pagination={false} columns={[ { title: t('ips.interface'), dataIndex: 'ifname', key: 'ifname', render: (s: string) => {s} }, { title: t('ips.address'), key: 'addr', render: (_, row: SystemAddress) => {row.address}/{row.prefix} }, { title: t('ips.family'), dataIndex: 'family', key: 'family', render: (f: string) => {f === 'inet' ? 'IPv4' : 'IPv6'} }, ]} /> ) } {t('ips.managedTitle')}
{ setEditing(null); setCreating(false) }} onOk={() => { void form.submit() }} confirmLoading={create.isPending || update.isPending} >
{ if (editing) update.mutate({ id: editing.id, v }) else create.mutate(v) }} > p.is_vip !== c.is_vip}> {({ getFieldValue }) => getFieldValue('is_vip') ? ( ) : null}
) }