fix(ui): Domain-Sync + HAProxy-Live-Stats in Backend-Detail
- Domain-Attachments werden beim Speichern korrekt sync't (syncDomainAttachments war in Detail.tsx vergessen worden) - Server-Tabelle in der Detail-Seite zeigt jetzt eine 'Live'-Spalte mit HAProxy-Status (UP/DOWN), aktive Sessions und Bytes per Hover- Tooltip; Daten kommen aus dem gemeinsamen ['haproxy','stats']-Query Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Button, Card, Col, Form, Input, InputNumber, Modal,
|
||||
Row, Select, Switch, Table, Tag, Typography, message,
|
||||
Row, Select, Switch, Table, Tag, Tooltip, Typography, message,
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { ArrowLeftOutlined, DatabaseOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
@@ -56,6 +56,26 @@ async function listDomains(): Promise<DomainFull[]> {
|
||||
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
|
||||
}
|
||||
|
||||
interface HAProxyStat {
|
||||
backend: string; server: string; status: string
|
||||
sessions: number; bytes_in: number; bytes_out: number
|
||||
last_change_sec: number; health: string
|
||||
}
|
||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
||||
try {
|
||||
const r = await apiClient.get('/haproxy/stats')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
export default function BackendDetailPage() {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
@@ -69,14 +89,40 @@ export default function BackendDetailPage() {
|
||||
enabled: !isNaN(backendID),
|
||||
})
|
||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||
const { data: haproxyStats } = useQuery({
|
||||
queryKey: ['haproxy', 'stats'],
|
||||
queryFn: listHAProxyStats,
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
const [form] = Form.useForm<BackendFormValues>()
|
||||
|
||||
async function syncDomainAttachments(selected: number[]) {
|
||||
const all = domains ?? []
|
||||
const wasAttached = new Set(all.filter(d => d.primary_backend_id === backendID).map(d => d.id))
|
||||
const want = new Set(selected)
|
||||
const puts: Promise<unknown>[] = []
|
||||
for (const id of [...want].filter(id => !wasAttached.has(id))) {
|
||||
const d = all.find(x => x.id === id)
|
||||
if (d) puts.push(apiClient.put(`/domains/${id}`, { ...d, primary_backend_id: backendID }))
|
||||
}
|
||||
for (const id of [...wasAttached].filter(id => !want.has(id))) {
|
||||
const d = all.find(x => x.id === id)
|
||||
if (d) puts.push(apiClient.put(`/domains/${id}`, { ...d, primary_backend_id: null }))
|
||||
}
|
||||
if (puts.length) await Promise.all(puts)
|
||||
}
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: async (v: BackendFormValues) => { await apiClient.put(`/backends/${backendID}`, v) },
|
||||
mutationFn: async (v: BackendFormValues) => {
|
||||
const { domain_ids, ...body } = v
|
||||
await apiClient.put(`/backends/${backendID}`, body)
|
||||
await syncDomainAttachments(domain_ids ?? [])
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t('common.save'))
|
||||
void qc.invalidateQueries({ queryKey: ['backends'] })
|
||||
void qc.invalidateQueries({ queryKey: ['backend', backendID] })
|
||||
void qc.invalidateQueries({ queryKey: ['domains'] })
|
||||
},
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
})
|
||||
@@ -164,7 +210,7 @@ export default function BackendDetailPage() {
|
||||
|
||||
<Col xs={24} lg={14}>
|
||||
<Card size="small" title={t('backends.serversIn', { name: backend.name })}>
|
||||
<ServerPanel backendID={backendID} />
|
||||
<ServerPanel backendID={backendID} haproxyStats={haproxyStats ?? []} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -172,7 +218,7 @@ export default function BackendDetailPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function ServerPanel({ backendID }: { backendID: number }) {
|
||||
function ServerPanel({ backendID, haproxyStats }: { backendID: number; haproxyStats: HAProxyStat[] }) {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -234,6 +280,22 @@ function ServerPanel({ backendID }: { backendID: number }) {
|
||||
title: t('backends.active'), dataIndex: 'active', width: 80,
|
||||
render: (v: boolean) => <StatusDot active={v} />,
|
||||
},
|
||||
{
|
||||
title: 'Live', key: 'live', width: 160,
|
||||
render: (_, r) => {
|
||||
const stat = haproxyStats.find(
|
||||
s => s.backend === `eg_backend_${backendID}` && s.server === r.name
|
||||
)
|
||||
if (!stat) return <Text type="secondary" style={{ fontSize: 11 }}>—</Text>
|
||||
const color = stat.status === 'UP' ? 'green' : stat.status === 'no check' ? 'default' : 'red'
|
||||
return (
|
||||
<Tooltip title={`↓${fmtBytes(stat.bytes_in)} ↑${fmtBytes(stat.bytes_out)} · ${stat.health || stat.status}`}>
|
||||
<Tag color={color} style={{ margin: 0, fontSize: 11 }}>{stat.status}</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>{stat.sessions} sess</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('common.actions'), key: 'a', width: 100,
|
||||
render: (_, r) => (
|
||||
|
||||
Reference in New Issue
Block a user