feat(ui): traffic tooltip (sess/bytes) auf Backend-Health-Chips
Backends- und Routing-Rules-Seite zeigen jetzt beim Hover auf den UP/DOWN-Chip die aggregierten HAProxy-Stats des jeweiligen Backends: aktive Sessions + kumulierte Bytes rein/raus seit letztem HAProxy-Start. Daten kommen aus dem bereits vorhandenen /haproxy/stats-Endpunkt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Alert, Button, Card, Form, Input, InputNumber, Modal, Popconfirm,
|
||||
Select, Space, Switch, Table, Tag, Typography, message,
|
||||
Select, Space, Switch, Table, Tag, Tooltip, Typography, message,
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { DatabaseOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
@@ -86,7 +86,17 @@ async function listDomains(): Promise<DomainFull[]> {
|
||||
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
|
||||
}
|
||||
|
||||
interface HAProxyStat { backend: string; server: string; status: string }
|
||||
interface HAProxyStat {
|
||||
backend: string; server: string; status: string
|
||||
sessions: number; bytes_in: number; bytes_out: 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 listHAProxyStats(): Promise<HAProxyStat[]> {
|
||||
try {
|
||||
const r = await apiClient.get('/haproxy/stats')
|
||||
@@ -118,6 +128,16 @@ export default function BackendsPage() {
|
||||
return 'DOWN'
|
||||
}
|
||||
|
||||
const backendTraffic = (id: number) => {
|
||||
const servers = (haproxyStats ?? []).filter(s => s.backend === `eg_backend_${id}`)
|
||||
if (!servers.length) return null
|
||||
return {
|
||||
sessions: servers.reduce((a, s) => a + (s.sessions ?? 0), 0),
|
||||
bytesIn: servers.reduce((a, s) => a + (s.bytes_in ?? 0), 0),
|
||||
bytesOut: servers.reduce((a, s) => a + (s.bytes_out ?? 0), 0),
|
||||
}
|
||||
}
|
||||
|
||||
// server-counts pro Backend laden wir lazy bei Expansion; in der
|
||||
// Tabelle reicht ein Hinweis ob 0 / N Server.
|
||||
const { data: serverCounts } = useQuery({
|
||||
@@ -243,7 +263,15 @@ export default function BackendsPage() {
|
||||
const s = backendLiveStatus(row.id)
|
||||
if (!s) return <Text type="secondary" style={{ fontSize: 12 }}>—</Text>
|
||||
const color = s === 'UP' ? 'green' : s === 'DEGRADED' ? 'orange' : 'red'
|
||||
return <Tag color={color} style={{ margin: 0 }}>{s}</Tag>
|
||||
const tr = backendTraffic(row.id)
|
||||
const tip = tr
|
||||
? `${tr.sessions} sess · ↓${fmtBytes(tr.bytesIn)} ↑${fmtBytes(tr.bytesOut)}`
|
||||
: undefined
|
||||
return (
|
||||
<Tooltip title={tip}>
|
||||
<Tag color={color} style={{ margin: 0 }}>{s}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ title: t('backends.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, message } from 'antd'
|
||||
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, message } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { BranchesOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -50,7 +50,14 @@ async function listBackends(): Promise<Backend[]> {
|
||||
return (r.data.data as { backends?: Backend[] }).backends ?? []
|
||||
}
|
||||
|
||||
interface HAProxyStat { backend: string; status: string }
|
||||
interface HAProxyStat { backend: string; status: string; sessions: number; bytes_in: number; bytes_out: 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 listHAProxyStats(): Promise<HAProxyStat[]> {
|
||||
try {
|
||||
const r = await apiClient.get('/haproxy/stats')
|
||||
@@ -77,11 +84,15 @@ export default function RoutingRulesPage() {
|
||||
const b = backends?.find((x) => x.id === id)
|
||||
return b ? `${b.name} (${b.address}:${b.port})` : `#${id}`
|
||||
}
|
||||
const backendHealth = (id: number): 'UP' | 'DOWN' | null => {
|
||||
const backendHealth = (id: number) => {
|
||||
if (!haproxyStats?.length) return null
|
||||
const servers = haproxyStats.filter(s => s.backend === `eg_backend_${id}`)
|
||||
if (!servers.length) return null
|
||||
return servers.some(s => s.status === 'UP') ? 'UP' : 'DOWN'
|
||||
const up = servers.some(s => s.status === 'UP')
|
||||
const sessions = servers.reduce((a, s) => a + (s.sessions ?? 0), 0)
|
||||
const bytesIn = servers.reduce((a, s) => a + (s.bytes_in ?? 0), 0)
|
||||
const bytesOut = servers.reduce((a, s) => a + (s.bytes_out ?? 0), 0)
|
||||
return { up, sessions, bytesIn, bytesOut }
|
||||
}
|
||||
|
||||
const [editing, setEditing] = useState<RoutingRule | null>(null)
|
||||
@@ -119,12 +130,16 @@ export default function RoutingRulesPage() {
|
||||
{
|
||||
title: t('routing.backend'), dataIndex: 'backend_id', key: 'backend',
|
||||
render: (id: number) => {
|
||||
const health = backendHealth(id)
|
||||
const h = backendHealth(id)
|
||||
const tip = h ? `${h.sessions} sess · ↓${fmtBytes(h.bytesIn)} ↑${fmtBytes(h.bytesOut)}` : undefined
|
||||
return (
|
||||
<Space size={4}>
|
||||
<span>{backendLabel(id)}</span>
|
||||
{health === 'UP' && <Tag color="green" style={{ margin: 0 }}>UP</Tag>}
|
||||
{health === 'DOWN' && <Tag color="red" style={{ margin: 0 }}>DOWN</Tag>}
|
||||
{h && (
|
||||
<Tooltip title={tip}>
|
||||
<Tag color={h.up ? 'green' : 'red'} style={{ margin: 0 }}>{h.up ? 'UP' : 'DOWN'}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user