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 { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Alert, Button, Card, Form, Input, InputNumber, Modal, Popconfirm,
|
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'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { DatabaseOutlined, PlusOutlined } from '@ant-design/icons'
|
import { DatabaseOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
@@ -86,7 +86,17 @@ async function listDomains(): Promise<DomainFull[]> {
|
|||||||
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
|
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[]> {
|
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
@@ -118,6 +128,16 @@ export default function BackendsPage() {
|
|||||||
return 'DOWN'
|
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
|
// server-counts pro Backend laden wir lazy bei Expansion; in der
|
||||||
// Tabelle reicht ein Hinweis ob 0 / N Server.
|
// Tabelle reicht ein Hinweis ob 0 / N Server.
|
||||||
const { data: serverCounts } = useQuery({
|
const { data: serverCounts } = useQuery({
|
||||||
@@ -243,7 +263,15 @@ export default function BackendsPage() {
|
|||||||
const s = backendLiveStatus(row.id)
|
const s = backendLiveStatus(row.id)
|
||||||
if (!s) return <Text type="secondary" style={{ fontSize: 12 }}>—</Text>
|
if (!s) return <Text type="secondary" style={{ fontSize: 12 }}>—</Text>
|
||||||
const color = s === 'UP' ? 'green' : s === 'DEGRADED' ? 'orange' : 'red'
|
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} /> },
|
{ title: t('backends.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
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 type { ColumnsType } from 'antd/es/table'
|
||||||
import { BranchesOutlined, PlusOutlined } from '@ant-design/icons'
|
import { BranchesOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
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 ?? []
|
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[]> {
|
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
@@ -77,11 +84,15 @@ export default function RoutingRulesPage() {
|
|||||||
const b = backends?.find((x) => x.id === id)
|
const b = backends?.find((x) => x.id === id)
|
||||||
return b ? `${b.name} (${b.address}:${b.port})` : `#${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
|
if (!haproxyStats?.length) return null
|
||||||
const servers = haproxyStats.filter(s => s.backend === `eg_backend_${id}`)
|
const servers = haproxyStats.filter(s => s.backend === `eg_backend_${id}`)
|
||||||
if (!servers.length) return null
|
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)
|
const [editing, setEditing] = useState<RoutingRule | null>(null)
|
||||||
@@ -119,12 +130,16 @@ export default function RoutingRulesPage() {
|
|||||||
{
|
{
|
||||||
title: t('routing.backend'), dataIndex: 'backend_id', key: 'backend',
|
title: t('routing.backend'), dataIndex: 'backend_id', key: 'backend',
|
||||||
render: (id: number) => {
|
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 (
|
return (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<span>{backendLabel(id)}</span>
|
<span>{backendLabel(id)}</span>
|
||||||
{health === 'UP' && <Tag color="green" style={{ margin: 0 }}>UP</Tag>}
|
{h && (
|
||||||
{health === 'DOWN' && <Tag color="red" style={{ margin: 0 }}>DOWN</Tag>}
|
<Tooltip title={tip}>
|
||||||
|
<Tag color={h.up ? 'green' : 'red'} style={{ margin: 0 }}>{h.up ? 'UP' : 'DOWN'}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user