diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 4e333ca..0f161c1 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -42,7 +42,10 @@ }, "ips": { "title": "IP-Adressen", - "intro": "Adressen, die EdgeGuard verwaltet — inklusive VIPs für Cluster-Failover. Bindung an die deklarierten Interfaces oben.", + "intro": "Adressen, die das Betriebssystem zeigt (Read-only oben) plus die Adressen, die EdgeGuard zusätzlich verwaltet — inklusive VIPs für Cluster-Failover.", + "systemDiscovered": "Adressen am Kernel (read-only)", + "managedTitle": "Verwaltete Adressen", + "family": "Familie", "addAddress": "Adresse hinzufügen", "editAddress": "Adresse bearbeiten", "interface": "Interface", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index 6466c9e..ead9bd1 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -42,7 +42,10 @@ }, "ips": { "title": "IP addresses", - "intro": "Addresses managed by EdgeGuard — including VIPs that follow the active cluster node on failover.", + "intro": "Addresses the kernel currently has (read-only above) plus addresses EdgeGuard additionally manages — including VIPs that follow the active cluster node on failover.", + "systemDiscovered": "Kernel addresses (read-only)", + "managedTitle": "Managed addresses", + "family": "Family", "addAddress": "Add address", "editAddress": "Edit address", "interface": "Interface", diff --git a/management-ui/src/pages/IPAddresses/index.tsx b/management-ui/src/pages/IPAddresses/index.tsx index 681ea92..938514c 100644 --- a/management-ui/src/pages/IPAddresses/index.tsx +++ b/management-ui/src/pages/IPAddresses/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd' +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' @@ -19,6 +19,20 @@ interface IPAddress { 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 @@ -42,12 +56,38 @@ async function listIfaces(): Promise { 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) @@ -122,6 +162,27 @@ export default function IPAddressesPage() {
{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')}