Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84112d399b | ||
|
|
0f2fba4a62 |
@@ -74,12 +74,28 @@ interface HAProxyStat {
|
|||||||
req_tot: number; req_rate: number
|
req_tot: number; req_rate: number
|
||||||
last_change_sec: number; health: string
|
last_change_sec: number; health: string
|
||||||
}
|
}
|
||||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt,
|
||||||
|
// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier
|
||||||
|
// IMMER die vollstaendige Antwort cachen und erst per `select` auf die
|
||||||
|
// Backends reduzieren, die diese Seite braucht.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing
|
||||||
|
// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag —
|
||||||
|
// nach einem Wechsel hierher und zurueck riss das Dashboard mit
|
||||||
|
// "Cannot read properties of undefined (reading 'length')" die ganze
|
||||||
|
// Oberflaeche in die ErrorBoundary.
|
||||||
|
interface HAProxyStatsPayload {
|
||||||
|
backends: HAProxyStat[]
|
||||||
|
frontends: unknown[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
async function fetchHAProxyStats(): Promise<HAProxyStatsPayload> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return { backends: [], frontends: [] }
|
||||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
const d = r.data.data as Partial<HAProxyStatsPayload>
|
||||||
} catch { return [] }
|
return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error }
|
||||||
|
} catch { return { backends: [], frontends: [] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtBytes(n: number): string {
|
function fmtBytes(n: number): string {
|
||||||
@@ -112,7 +128,8 @@ export default function BackendDetailPage() {
|
|||||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||||
const { data: haproxyStats } = useQuery({
|
const { data: haproxyStats } = useQuery({
|
||||||
queryKey: ['haproxy', 'stats'],
|
queryKey: ['haproxy', 'stats'],
|
||||||
queryFn: listHAProxyStats,
|
queryFn: fetchHAProxyStats,
|
||||||
|
select: (d: HAProxyStatsPayload) => d.backends,
|
||||||
refetchInterval: 10_000,
|
refetchInterval: 10_000,
|
||||||
})
|
})
|
||||||
const [form] = Form.useForm<BackendFormValues>()
|
const [form] = Form.useForm<BackendFormValues>()
|
||||||
|
|||||||
@@ -118,12 +118,28 @@ function fmtBytes(n: number): string {
|
|||||||
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
||||||
return n + ' B'
|
return n + ' B'
|
||||||
}
|
}
|
||||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt,
|
||||||
|
// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier
|
||||||
|
// IMMER die vollstaendige Antwort cachen und erst per `select` auf die
|
||||||
|
// Backends reduzieren, die diese Seite braucht.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing
|
||||||
|
// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag —
|
||||||
|
// nach einem Wechsel hierher und zurueck riss das Dashboard mit
|
||||||
|
// "Cannot read properties of undefined (reading 'length')" die ganze
|
||||||
|
// Oberflaeche in die ErrorBoundary.
|
||||||
|
interface HAProxyStatsPayload {
|
||||||
|
backends: HAProxyStat[]
|
||||||
|
frontends: unknown[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
async function fetchHAProxyStats(): Promise<HAProxyStatsPayload> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return { backends: [], frontends: [] }
|
||||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
const d = r.data.data as Partial<HAProxyStatsPayload>
|
||||||
} catch { return [] }
|
return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error }
|
||||||
|
} catch { return { backends: [], frontends: [] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BackendsPage() {
|
export default function BackendsPage() {
|
||||||
@@ -146,7 +162,8 @@ export default function BackendsPage() {
|
|||||||
const haproxyService = services?.find(s => s.unit === 'haproxy.service' || s.unit === 'haproxy')
|
const haproxyService = services?.find(s => s.unit === 'haproxy.service' || s.unit === 'haproxy')
|
||||||
const { data: haproxyStats } = useQuery({
|
const { data: haproxyStats } = useQuery({
|
||||||
queryKey: ['haproxy', 'stats'],
|
queryKey: ['haproxy', 'stats'],
|
||||||
queryFn: listHAProxyStats,
|
queryFn: fetchHAProxyStats,
|
||||||
|
select: (d: HAProxyStatsPayload) => d.backends,
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -613,11 +613,11 @@ function VIPCard({ data }: { data?: VIPStatus | null }) {
|
|||||||
>
|
>
|
||||||
{!data ? (
|
{!data ? (
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>—</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>—</Text>
|
||||||
) : data.vips.length === 0 ? (
|
) : (data.vips ?? []).length === 0 ? (
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>{t('dashboard.vipCard.noVips')}</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>{t('dashboard.vipCard.noVips')}</Text>
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size={0}>
|
<Space direction="vertical" style={{ width: '100%' }} size={0}>
|
||||||
{data.vips.map((v) => (
|
{(data.vips ?? []).map((v) => (
|
||||||
<div key={v.address} style={{
|
<div key={v.address} style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
padding: '5px 0', borderBottom: '1px solid #F1F5F9',
|
padding: '5px 0', borderBottom: '1px solid #F1F5F9',
|
||||||
@@ -715,7 +715,7 @@ function HAProxyFullCard({ stats, resolveHAName }: HAProxyFullCardProps) {
|
|||||||
className="h-100"
|
className="h-100"
|
||||||
title={<><DatabaseOutlined style={{ color: '#0EA5E9' }} /> {t('dashboard.haproxyCard.title')}</>}
|
title={<><DatabaseOutlined style={{ color: '#0EA5E9' }} /> {t('dashboard.haproxyCard.title')}</>}
|
||||||
extra={
|
extra={
|
||||||
stats.frontends.length > 0 && (
|
(stats.frontends ?? []).length > 0 && (
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
<Text type="secondary" style={{ fontSize: 11 }}>{totalSessions} sess</Text>
|
<Text type="secondary" style={{ fontSize: 11 }}>{totalSessions} sess</Text>
|
||||||
{totalReqRate > 0 && <Text type="secondary" style={{ fontSize: 11 }}>{totalReqRate}/s</Text>}
|
{totalReqRate > 0 && <Text type="secondary" style={{ fontSize: 11 }}>{totalReqRate}/s</Text>}
|
||||||
@@ -731,7 +731,7 @@ function HAProxyFullCard({ stats, resolveHAName }: HAProxyFullCardProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Listeners */}
|
{/* Listeners */}
|
||||||
{stats.frontends.length > 0 && (
|
{(stats.frontends ?? []).length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
<Text type="secondary" style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
{t('dashboard.haproxyCard.frontends')}
|
{t('dashboard.haproxyCard.frontends')}
|
||||||
@@ -752,7 +752,7 @@ function HAProxyFullCard({ stats, resolveHAName }: HAProxyFullCardProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Backends */}
|
{/* Backends */}
|
||||||
{stats.backends.length === 0 && !stats.error ? (
|
{(stats.backends ?? []).length === 0 && !stats.error ? (
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>{t('dashboard.haproxyCard.empty')}</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>{t('dashboard.haproxyCard.empty')}</Text>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -101,12 +101,28 @@ async function listCerts(): Promise<TLSCertLite[]> {
|
|||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return []
|
||||||
return (r.data.data as { tls_certs?: TLSCertLite[] }).tls_certs ?? []
|
return (r.data.data as { tls_certs?: TLSCertLite[] }).tls_certs ?? []
|
||||||
}
|
}
|
||||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt,
|
||||||
|
// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier
|
||||||
|
// IMMER die vollstaendige Antwort cachen und erst per `select` auf die
|
||||||
|
// Backends reduzieren, die diese Seite braucht.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing
|
||||||
|
// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag —
|
||||||
|
// nach einem Wechsel hierher und zurueck riss das Dashboard mit
|
||||||
|
// "Cannot read properties of undefined (reading 'length')" die ganze
|
||||||
|
// Oberflaeche in die ErrorBoundary.
|
||||||
|
interface HAProxyStatsPayload {
|
||||||
|
backends: HAProxyStat[]
|
||||||
|
frontends: unknown[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
async function fetchHAProxyStats(): Promise<HAProxyStatsPayload> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return { backends: [], frontends: [] }
|
||||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
const d = r.data.data as Partial<HAProxyStatsPayload>
|
||||||
} catch { return [] }
|
return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error }
|
||||||
|
} catch { return { backends: [], frontends: [] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DomainDetailPage() {
|
export default function DomainDetailPage() {
|
||||||
@@ -126,7 +142,8 @@ export default function DomainDetailPage() {
|
|||||||
const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
||||||
const { data: haproxyStats } = useQuery({
|
const { data: haproxyStats } = useQuery({
|
||||||
queryKey: ['haproxy', 'stats'],
|
queryKey: ['haproxy', 'stats'],
|
||||||
queryFn: listHAProxyStats,
|
queryFn: fetchHAProxyStats,
|
||||||
|
select: (d: HAProxyStatsPayload) => d.backends,
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -83,12 +83,28 @@ async function listCerts(): Promise<TLSCertLite[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HAProxyStat { backend: string; server: string; status: string }
|
interface HAProxyStat { backend: string; server: string; status: string }
|
||||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt,
|
||||||
|
// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier
|
||||||
|
// IMMER die vollstaendige Antwort cachen und erst per `select` auf die
|
||||||
|
// Backends reduzieren, die diese Seite braucht.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing
|
||||||
|
// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag —
|
||||||
|
// nach einem Wechsel hierher und zurueck riss das Dashboard mit
|
||||||
|
// "Cannot read properties of undefined (reading 'length')" die ganze
|
||||||
|
// Oberflaeche in die ErrorBoundary.
|
||||||
|
interface HAProxyStatsPayload {
|
||||||
|
backends: HAProxyStat[]
|
||||||
|
frontends: unknown[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
async function fetchHAProxyStats(): Promise<HAProxyStatsPayload> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return { backends: [], frontends: [] }
|
||||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
const d = r.data.data as Partial<HAProxyStatsPayload>
|
||||||
} catch { return [] }
|
return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error }
|
||||||
|
} catch { return { backends: [], frontends: [] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DomainsPage() {
|
export default function DomainsPage() {
|
||||||
@@ -109,7 +125,8 @@ export default function DomainsPage() {
|
|||||||
const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
const { data: certs } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
||||||
const { data: haproxyStats } = useQuery({
|
const { data: haproxyStats } = useQuery({
|
||||||
queryKey: ['haproxy', 'stats'],
|
queryKey: ['haproxy', 'stats'],
|
||||||
queryFn: listHAProxyStats,
|
queryFn: fetchHAProxyStats,
|
||||||
|
select: (d: HAProxyStatsPayload) => d.backends,
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
})
|
})
|
||||||
const certByDomain = new Map((certs ?? []).map(c => [c.domain, c]))
|
const certByDomain = new Map((certs ?? []).map(c => [c.domain, c]))
|
||||||
|
|||||||
@@ -58,12 +58,28 @@ function fmtBytes(n: number): string {
|
|||||||
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
if (n >= 1_024) return (n / 1_024).toFixed(0) + ' KB'
|
||||||
return n + ' B'
|
return n + ' B'
|
||||||
}
|
}
|
||||||
async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
// Der Cache-Eintrag ['haproxy','stats'] wird mit dem Dashboard geteilt,
|
||||||
|
// das aus derselben Antwort zusaetzlich `frontends` liest. Deshalb hier
|
||||||
|
// IMMER die vollstaendige Antwort cachen und erst per `select` auf die
|
||||||
|
// Backends reduzieren, die diese Seite braucht.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Lieferte diese Funktion nur das Backend-Array, hing
|
||||||
|
// es vom zuletzt besuchten Screen ab, welche Form unter dem Key lag —
|
||||||
|
// nach einem Wechsel hierher und zurueck riss das Dashboard mit
|
||||||
|
// "Cannot read properties of undefined (reading 'length')" die ganze
|
||||||
|
// Oberflaeche in die ErrorBoundary.
|
||||||
|
interface HAProxyStatsPayload {
|
||||||
|
backends: HAProxyStat[]
|
||||||
|
frontends: unknown[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
async function fetchHAProxyStats(): Promise<HAProxyStatsPayload> {
|
||||||
try {
|
try {
|
||||||
const r = await apiClient.get('/haproxy/stats')
|
const r = await apiClient.get('/haproxy/stats')
|
||||||
if (!isEnvelope(r.data)) return []
|
if (!isEnvelope(r.data)) return { backends: [], frontends: [] }
|
||||||
return (r.data.data as { backends?: HAProxyStat[] }).backends ?? []
|
const d = r.data.data as Partial<HAProxyStatsPayload>
|
||||||
} catch { return [] }
|
return { backends: d.backends ?? [], frontends: d.frontends ?? [], error: d.error }
|
||||||
|
} catch { return { backends: [], frontends: [] } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RoutingRulesPage() {
|
export default function RoutingRulesPage() {
|
||||||
@@ -76,7 +92,8 @@ export default function RoutingRulesPage() {
|
|||||||
const { data: backends } = useQuery({ queryKey: ['backends'], queryFn: listBackends })
|
const { data: backends } = useQuery({ queryKey: ['backends'], queryFn: listBackends })
|
||||||
const { data: haproxyStats } = useQuery({
|
const { data: haproxyStats } = useQuery({
|
||||||
queryKey: ['haproxy', 'stats'],
|
queryKey: ['haproxy', 'stats'],
|
||||||
queryFn: listHAProxyStats,
|
queryFn: fetchHAProxyStats,
|
||||||
|
select: (d: HAProxyStatsPayload) => d.backends,
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user