feat(networks): Live-Traffic-Zähler in System-Interfaces-Card
GET /system/interfaces liefert jetzt rx_bytes/tx_bytes/rx_packets/ tx_packets/rx_drop/tx_drop aus /proc/net/dev. Die System-Interfaces- Card in Netzwerk → Interfaces zeigt die Werte als Mini-Tabelle mit 10s-Refetch; Hover auf den Bytes-Werten zeigt Paketzähler + Drops. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -138,7 +138,10 @@
|
||||
"interfaces": "Interfaces",
|
||||
"routes": "Routen"
|
||||
},
|
||||
"systemDiscovered": "System-Interfaces (read-only)",
|
||||
"systemDiscovered": "System-Interfaces — Live-Traffic-Zähler (seit letztem HAProxy-Start)",
|
||||
"addresses": "Adressen",
|
||||
"linkType": "Typ",
|
||||
"systemEmpty": "Keine Kernel-Interfaces gefunden",
|
||||
"addInterface": "Interface hinzufügen",
|
||||
"editInterface": "Interface bearbeiten",
|
||||
"emptyTitle": "Noch keine verwalteten Interfaces.",
|
||||
|
||||
@@ -138,7 +138,10 @@
|
||||
"interfaces": "Interfaces",
|
||||
"routes": "Routes"
|
||||
},
|
||||
"systemDiscovered": "System interfaces (read-only)",
|
||||
"systemDiscovered": "System interfaces — live traffic counters (since last HAProxy start)",
|
||||
"addresses": "Addresses",
|
||||
"linkType": "Type",
|
||||
"systemEmpty": "No kernel interfaces found",
|
||||
"addInterface": "Add interface",
|
||||
"editInterface": "Edit interface",
|
||||
"emptyTitle": "No managed interfaces yet.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, Typography, message } from 'antd'
|
||||
import { Button, Card, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Tag, Tooltip, Typography, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { ClusterOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -44,6 +44,16 @@ interface SystemInterface {
|
||||
address?: string
|
||||
flags?: string[]
|
||||
addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }>
|
||||
rx_bytes: number; tx_bytes: number
|
||||
rx_packets: number; tx_packets: number
|
||||
rx_drop: number; tx_drop: number
|
||||
}
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (n >= 1_073_741_824) return (n / 1_073_741_824).toFixed(1) + ' GB'
|
||||
if (n >= 1_048_576) return (n / 1_048_576).toFixed(1) + ' MB'
|
||||
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
||||
return n + ' B'
|
||||
}
|
||||
|
||||
async function listInterfaces(): Promise<NetworkInterface[]> {
|
||||
@@ -70,7 +80,7 @@ export default function InterfacesTab() {
|
||||
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 { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces, refetchInterval: 10_000 })
|
||||
const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones })
|
||||
|
||||
const [editing, setEditing] = useState<NetworkInterface | null>(null)
|
||||
@@ -158,18 +168,48 @@ export default function InterfacesTab() {
|
||||
return (
|
||||
<div>
|
||||
<Card title={t('networks.systemDiscovered')} className="mb-12" size="small">
|
||||
<Space wrap>
|
||||
{(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 (
|
||||
<Tooltip key={i.ifname} title={[...v4, ...v6].join(' · ') || '—'}>
|
||||
<Tag>{i.ifname}{v4[0] ? ` · ${v4[0]}` : ''}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
{(sys ?? []).length === 0 && <Typography.Text type="secondary">—</Typography.Text>}
|
||||
</Space>
|
||||
<Table
|
||||
size="small"
|
||||
dataSource={(sys ?? []).filter(i => i.ifname !== 'lo')}
|
||||
rowKey="ifname"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: t('networks.name'), dataIndex: 'ifname', key: 'ifname', width: 120,
|
||||
render: (s: string) => <code>{s}</code>,
|
||||
},
|
||||
{
|
||||
title: t('networks.addresses'), key: 'addrs',
|
||||
render: (_, row: SystemInterface) => {
|
||||
const addrs = (row.addr_info ?? []).map(a => `${a.local}/${a.prefixlen}`)
|
||||
return addrs.length
|
||||
? <Space size={4} wrap>{addrs.map(a => <Tag key={a} style={{ fontFamily: 'monospace', fontSize: 11 }}>{a}</Tag>)}</Space>
|
||||
: <Typography.Text type="secondary">—</Typography.Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '▼ RX', key: 'rx', width: 130,
|
||||
render: (_, row: SystemInterface) => (
|
||||
<Tooltip title={`${row.rx_packets?.toLocaleString()} pkts${row.rx_drop ? ` · ${row.rx_drop} drop` : ''}`}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{fmtBytes(row.rx_bytes ?? 0)}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '▲ TX', key: 'tx', width: 130,
|
||||
render: (_, row: SystemInterface) => (
|
||||
<Tooltip title={`${row.tx_packets?.toLocaleString()} pkts${row.tx_drop ? ` · ${row.tx_drop} drop` : ''}`}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{fmtBytes(row.tx_bytes ?? 0)}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('networks.linkType'), dataIndex: 'link_type', key: 'link_type', width: 90,
|
||||
render: (v?: string) => v ? <Tag>{v}</Tag> : '—',
|
||||
},
|
||||
]}
|
||||
locale={{ emptyText: t('networks.systemEmpty') }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<DataTable
|
||||
|
||||
Reference in New Issue
Block a user