fix(web): parse string readiness checks on monitoring System tab
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 38s
CI / web (push) Successful in 54s
CI / release (push) Successful in 3m59s

Исправляет ложные «Ошибки проверок»: GET /v1/ready отдаёт строки ok/memory, а не boolean. Уплотнён ReUI Frame/donut layout на вкладке Система.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-12 13:41:58 +07:00
co-authored by Cursor
parent 32d9dc6acb
commit 6a25a3d137
8 changed files with 235 additions and 67 deletions
@@ -40,7 +40,7 @@ export function ChartDonutMetric({
}
return (
<div className={cn('flex items-center gap-6', className)}>
<div className={cn('flex flex-col items-center justify-start gap-4 sm:flex-row sm:gap-6', className)}>
<ChartContainer config={chartConfig} className="mx-0 aspect-square h-44 w-44 shrink-0">
<PieChart>
<Pie
@@ -75,7 +75,7 @@ export function ChartDonutMetric({
</PieChart>
</ChartContainer>
<ul className="min-w-0 flex-1 space-y-3">
<ul className="flex w-full min-w-0 max-w-xs flex-col gap-3 sm:w-auto sm:min-w-[10rem]">
{slices.map((slice) => {
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
return (
@@ -7,12 +7,20 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { jobStatusRu, readyCheckRu } from '@/lib/ui-labels'
import {
isReadyCheckOk,
isSystemReady,
readyCheckStatusLabel,
} from '@/lib/metrics'
import { readyCheckRu } from '@/lib/ui-labels'
import type { ReadyStatus } from '@/queries/monitoring'
import { Item, ItemMedia } from '@evobgp/ui/components/item'
import { cn } from '@evobgp/ui/lib/utils'
const READY_CHECK_ICONS: Record<string, typeof Database> = {
postgres: Database,
store: HardDrive,
store_backend: HardDrive,
jobs: ListTodo,
}
@@ -21,6 +29,7 @@ interface ReadyCheckRow {
label: string
subtitle?: string
icon: typeof Database
iconClassName?: string
status: string
statusLabel: string
}
@@ -34,12 +43,14 @@ export function MonitoringReadyGrid({
}) {
const data = useMemo<ReadyCheckRow[]>(() => {
const checks = ready.checks ?? {}
const systemReady = isSystemReady(ready.status)
const rows: ReadyCheckRow[] = [
{
id: 'liveness',
label: 'Живучесть',
subtitle: '/v1/health',
icon: HeartPulse,
iconClassName: health?.ok ? 'text-success' : 'text-destructive',
status: health?.ok ? 'ok' : 'error',
statusLabel: health?.ok ? 'В норме' : 'Ошибка',
},
@@ -48,19 +59,21 @@ export function MonitoringReadyGrid({
label: 'Готовность',
subtitle: '/v1/ready',
icon: ShieldCheck,
status: ready.status === 'ok' ? 'ok' : 'warning',
statusLabel: ready.status === 'ok' ? 'Готов' : jobStatusRu(ready.status ?? 'pending'),
iconClassName: systemReady ? 'text-success' : 'text-warning',
status: systemReady ? 'ok' : 'warning',
statusLabel: systemReady ? 'Готов' : 'Не готов',
},
]
for (const key of Object.keys(checks)) {
const value = checks[key]
const ok = typeof value === 'boolean' ? value : value?.ok !== false
const ok = isReadyCheckOk(value)
rows.push({
id: key,
label: readyCheckRu(key),
icon: READY_CHECK_ICONS[key] ?? ListTodo,
iconClassName: ok ? 'text-success' : 'text-destructive',
status: ok ? 'ok' : 'error',
statusLabel: ok ? 'В норме' : 'Ошибка',
statusLabel: readyCheckStatusLabel(value, ok),
})
}
return rows
@@ -74,8 +87,17 @@ export function MonitoringReadyGrid({
cell: ({ row }) => {
const Icon = row.original.icon
return (
<div className="flex items-center gap-2">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<div className="flex min-w-0 items-center gap-2.5 py-0.5">
<Item
className={cn(
'border-background bg-muted flex size-8 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-3.5',
row.original.iconClassName,
)}
>
<ItemMedia variant="icon" className="size-auto">
<Icon />
</ItemMedia>
</Item>
<DataGridPrimaryCell
title={row.original.label}
subtitle={row.original.subtitle}
@@ -90,7 +112,9 @@ export function MonitoringReadyGrid({
enableSorting: false,
header: 'Статус',
cell: ({ row }) => (
<StatusBadge status={row.original.status} label={row.original.statusLabel} />
<div className="flex justify-start">
<StatusBadge status={row.original.status} label={row.original.statusLabel} />
</div>
),
meta: { headerTitle: 'Статус' },
},
@@ -8,7 +8,7 @@ import {
} from '@evobgp/ui/components/chart'
import type { BreakdownSlice } from '@/lib/metrics'
/** chart-27 / chart-13 inspired donut in PanelCard. */
/** chart-27 / chart-13 inspired donut in PanelCard (Frame surface). */
export function DonutBreakdownCard({
title,
description,
@@ -30,13 +30,24 @@ export function DonutBreakdownCard({
const data = slices.map((slice) => ({ ...slice, fill: slice.color, share: slice.count }))
return (
<PanelCard title={title} description={description} className="h-full">
<div className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center">
<PanelCard
title={title}
description={description}
className="h-full"
actions={
badge ? (
<Badge variant="success-light" className="hidden sm:inline-flex">
{badge}
</Badge>
) : undefined
}
>
<div className="flex flex-col items-center justify-start gap-4 p-4 sm:flex-row sm:items-center sm:justify-start sm:gap-6">
{total === 0 ? (
<p className="text-muted-foreground w-full py-8 text-center text-sm">Нет данных</p>
) : (
<>
<div className="relative mx-auto size-36 shrink-0">
<div className="relative mx-auto size-36 shrink-0 sm:mx-0">
<ChartContainer config={chartConfig} className="aspect-square size-36">
<PieChart>
<Pie
@@ -59,11 +70,11 @@ export function DonutBreakdownCard({
<span className="text-lg font-semibold tabular-nums">{total}</span>
</div>
</div>
<ul className="min-w-0 flex-1 space-y-2">
<ul className="flex w-full min-w-0 max-w-xs flex-col gap-2 sm:w-auto sm:min-w-[10rem]">
{slices.map((slice) => {
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
return (
<li key={slice.key} className="flex items-center justify-between gap-2 text-sm">
<li key={slice.key} className="flex items-center justify-between gap-3 text-sm">
<span className="flex min-w-0 items-center gap-2">
<span
className="size-2.5 shrink-0 rounded-full"
@@ -81,11 +92,6 @@ export function DonutBreakdownCard({
</ul>
</>
)}
{badge ? (
<Badge variant="success-light" className="absolute top-4 right-4 hidden sm:flex">
{badge}
</Badge>
) : null}
</div>
</PanelCard>
)