feat(ui): HAProxy-Health-Chip in Routing-Rules-Tabelle
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd'
|
import { Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, 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,6 +50,15 @@ 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 }
|
||||||
|
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 [] }
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoutingRulesPage() {
|
export default function RoutingRulesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
@@ -57,12 +66,23 @@ export default function RoutingRulesPage() {
|
|||||||
const { data: rules, isLoading } = useQuery({ queryKey: ['routing-rules'], queryFn: listRules })
|
const { data: rules, isLoading } = useQuery({ queryKey: ['routing-rules'], queryFn: listRules })
|
||||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||||
const { data: backends } = useQuery({ queryKey: ['backends'], queryFn: listBackends })
|
const { data: backends } = useQuery({ queryKey: ['backends'], queryFn: listBackends })
|
||||||
|
const { data: haproxyStats } = useQuery({
|
||||||
|
queryKey: ['haproxy', 'stats'],
|
||||||
|
queryFn: listHAProxyStats,
|
||||||
|
refetchInterval: 15_000,
|
||||||
|
})
|
||||||
|
|
||||||
const domainName = (id: number) => domains?.find((d) => d.id === id)?.name ?? `#${id}`
|
const domainName = (id: number) => domains?.find((d) => d.id === id)?.name ?? `#${id}`
|
||||||
const backendLabel = (id: number) => {
|
const backendLabel = (id: number) => {
|
||||||
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 => {
|
||||||
|
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 [editing, setEditing] = useState<RoutingRule | null>(null)
|
const [editing, setEditing] = useState<RoutingRule | null>(null)
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
@@ -96,7 +116,19 @@ export default function RoutingRulesPage() {
|
|||||||
const columns: ColumnsType<RoutingRule> = [
|
const columns: ColumnsType<RoutingRule> = [
|
||||||
{ title: t('routing.domain'), dataIndex: 'domain_id', key: 'domain', render: (id: number) => domainName(id) },
|
{ title: t('routing.domain'), dataIndex: 'domain_id', key: 'domain', render: (id: number) => domainName(id) },
|
||||||
{ title: t('routing.pathPrefix'), dataIndex: 'path_prefix', key: 'path' },
|
{ title: t('routing.pathPrefix'), dataIndex: 'path_prefix', key: 'path' },
|
||||||
{ title: t('routing.backend'), dataIndex: 'backend_id', key: 'backend', render: (id: number) => backendLabel(id) },
|
{
|
||||||
|
title: t('routing.backend'), dataIndex: 'backend_id', key: 'backend',
|
||||||
|
render: (id: number) => {
|
||||||
|
const health = backendHealth(id)
|
||||||
|
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>}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{ title: t('routing.priority'), dataIndex: 'priority', key: 'priority' },
|
{ title: t('routing.priority'), dataIndex: 'priority', key: 'priority' },
|
||||||
{ title: t('routing.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> },
|
{ title: t('routing.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> },
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user