Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a25a3d137 |
@@ -40,7 +40,7 @@ export function ChartDonutMetric({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<ChartContainer config={chartConfig} className="mx-0 aspect-square h-44 w-44 shrink-0">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
@@ -75,7 +75,7 @@ export function ChartDonutMetric({
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ChartContainer>
|
</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) => {
|
{slices.map((slice) => {
|
||||||
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
|
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ import { DataGridSection } from '@/components/data-grid-shell'
|
|||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||||
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
|
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 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> = {
|
const READY_CHECK_ICONS: Record<string, typeof Database> = {
|
||||||
postgres: Database,
|
postgres: Database,
|
||||||
store: HardDrive,
|
store: HardDrive,
|
||||||
|
store_backend: HardDrive,
|
||||||
jobs: ListTodo,
|
jobs: ListTodo,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +29,7 @@ interface ReadyCheckRow {
|
|||||||
label: string
|
label: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
icon: typeof Database
|
icon: typeof Database
|
||||||
|
iconClassName?: string
|
||||||
status: string
|
status: string
|
||||||
statusLabel: string
|
statusLabel: string
|
||||||
}
|
}
|
||||||
@@ -34,12 +43,14 @@ export function MonitoringReadyGrid({
|
|||||||
}) {
|
}) {
|
||||||
const data = useMemo<ReadyCheckRow[]>(() => {
|
const data = useMemo<ReadyCheckRow[]>(() => {
|
||||||
const checks = ready.checks ?? {}
|
const checks = ready.checks ?? {}
|
||||||
|
const systemReady = isSystemReady(ready.status)
|
||||||
const rows: ReadyCheckRow[] = [
|
const rows: ReadyCheckRow[] = [
|
||||||
{
|
{
|
||||||
id: 'liveness',
|
id: 'liveness',
|
||||||
label: 'Живучесть',
|
label: 'Живучесть',
|
||||||
subtitle: '/v1/health',
|
subtitle: '/v1/health',
|
||||||
icon: HeartPulse,
|
icon: HeartPulse,
|
||||||
|
iconClassName: health?.ok ? 'text-success' : 'text-destructive',
|
||||||
status: health?.ok ? 'ok' : 'error',
|
status: health?.ok ? 'ok' : 'error',
|
||||||
statusLabel: health?.ok ? 'В норме' : 'Ошибка',
|
statusLabel: health?.ok ? 'В норме' : 'Ошибка',
|
||||||
},
|
},
|
||||||
@@ -48,19 +59,21 @@ export function MonitoringReadyGrid({
|
|||||||
label: 'Готовность',
|
label: 'Готовность',
|
||||||
subtitle: '/v1/ready',
|
subtitle: '/v1/ready',
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
status: ready.status === 'ok' ? 'ok' : 'warning',
|
iconClassName: systemReady ? 'text-success' : 'text-warning',
|
||||||
statusLabel: ready.status === 'ok' ? 'Готов' : jobStatusRu(ready.status ?? 'pending'),
|
status: systemReady ? 'ok' : 'warning',
|
||||||
|
statusLabel: systemReady ? 'Готов' : 'Не готов',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
for (const key of Object.keys(checks)) {
|
for (const key of Object.keys(checks)) {
|
||||||
const value = checks[key]
|
const value = checks[key]
|
||||||
const ok = typeof value === 'boolean' ? value : value?.ok !== false
|
const ok = isReadyCheckOk(value)
|
||||||
rows.push({
|
rows.push({
|
||||||
id: key,
|
id: key,
|
||||||
label: readyCheckRu(key),
|
label: readyCheckRu(key),
|
||||||
icon: READY_CHECK_ICONS[key] ?? ListTodo,
|
icon: READY_CHECK_ICONS[key] ?? ListTodo,
|
||||||
|
iconClassName: ok ? 'text-success' : 'text-destructive',
|
||||||
status: ok ? 'ok' : 'error',
|
status: ok ? 'ok' : 'error',
|
||||||
statusLabel: ok ? 'В норме' : 'Ошибка',
|
statusLabel: readyCheckStatusLabel(value, ok),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
@@ -74,8 +87,17 @@ export function MonitoringReadyGrid({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const Icon = row.original.icon
|
const Icon = row.original.icon
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2.5 py-0.5">
|
||||||
<Icon className="size-4 shrink-0 text-muted-foreground" />
|
<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
|
<DataGridPrimaryCell
|
||||||
title={row.original.label}
|
title={row.original.label}
|
||||||
subtitle={row.original.subtitle}
|
subtitle={row.original.subtitle}
|
||||||
@@ -90,7 +112,9 @@ export function MonitoringReadyGrid({
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
header: 'Статус',
|
header: 'Статус',
|
||||||
cell: ({ row }) => (
|
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: 'Статус' },
|
meta: { headerTitle: 'Статус' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@evobgp/ui/components/chart'
|
} from '@evobgp/ui/components/chart'
|
||||||
import type { BreakdownSlice } from '@/lib/metrics'
|
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({
|
export function DonutBreakdownCard({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -30,13 +30,24 @@ export function DonutBreakdownCard({
|
|||||||
const data = slices.map((slice) => ({ ...slice, fill: slice.color, share: slice.count }))
|
const data = slices.map((slice) => ({ ...slice, fill: slice.color, share: slice.count }))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelCard title={title} description={description} className="h-full">
|
<PanelCard
|
||||||
<div className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center">
|
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 ? (
|
{total === 0 ? (
|
||||||
<p className="text-muted-foreground w-full py-8 text-center text-sm">Нет данных</p>
|
<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">
|
<ChartContainer config={chartConfig} className="aspect-square size-36">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
@@ -59,11 +70,11 @@ export function DonutBreakdownCard({
|
|||||||
<span className="text-lg font-semibold tabular-nums">{total}</span>
|
<span className="text-lg font-semibold tabular-nums">{total}</span>
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
{slices.map((slice) => {
|
||||||
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
|
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
|
||||||
return (
|
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="flex min-w-0 items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="size-2.5 shrink-0 rounded-full"
|
className="size-2.5 shrink-0 rounded-full"
|
||||||
@@ -81,11 +92,6 @@ export function DonutBreakdownCard({
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{badge ? (
|
|
||||||
<Badge variant="success-light" className="absolute top-4 right-4 hidden sm:flex">
|
|
||||||
{badge}
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
isReadyCheckOk,
|
||||||
|
isSystemReady,
|
||||||
|
readinessBreakdown,
|
||||||
|
readyCheckStatusLabel,
|
||||||
|
} from '@/lib/metrics/readiness-breakdown'
|
||||||
|
|
||||||
|
describe('isSystemReady', () => {
|
||||||
|
it('accepts ready and ok', () => {
|
||||||
|
expect(isSystemReady('ready')).toBe(true)
|
||||||
|
expect(isSystemReady('ok')).toBe(true)
|
||||||
|
expect(isSystemReady('READY')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects not_ready and empty', () => {
|
||||||
|
expect(isSystemReady('not_ready')).toBe(false)
|
||||||
|
expect(isSystemReady(undefined)).toBe(false)
|
||||||
|
expect(isSystemReady('')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isReadyCheckOk', () => {
|
||||||
|
it('parses backend string checks as ok', () => {
|
||||||
|
expect(isReadyCheckOk('ok')).toBe(true)
|
||||||
|
expect(isReadyCheckOk('memory')).toBe(true)
|
||||||
|
expect(isReadyCheckOk('unavailable')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses boolean and object forms', () => {
|
||||||
|
expect(isReadyCheckOk(true)).toBe(true)
|
||||||
|
expect(isReadyCheckOk(false)).toBe(false)
|
||||||
|
expect(isReadyCheckOk({ ok: true })).toBe(true)
|
||||||
|
expect(isReadyCheckOk({ ok: false, error: 'down' })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('readyCheckStatusLabel', () => {
|
||||||
|
it('labels memory and failures', () => {
|
||||||
|
expect(readyCheckStatusLabel('memory', true)).toBe('Memory')
|
||||||
|
expect(readyCheckStatusLabel('ok', true)).toBe('В норме')
|
||||||
|
expect(readyCheckStatusLabel('unavailable', false)).toBe('Недоступно')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('readinessBreakdown', () => {
|
||||||
|
it('counts healthy handleReady payload without false failures', () => {
|
||||||
|
const slices = readinessBreakdown(
|
||||||
|
{
|
||||||
|
status: 'ready',
|
||||||
|
checks: { store: 'ok', jobs: 'memory', postgres: 'ok' },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(slices.find((s) => s.key === 'checks-fail')).toBeUndefined()
|
||||||
|
expect(slices.find((s) => s.key === 'checks-ok')?.count).toBe(3)
|
||||||
|
expect(slices.find((s) => s.key === 'health')?.count).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts unavailable checks as failures', () => {
|
||||||
|
const slices = readinessBreakdown(
|
||||||
|
{
|
||||||
|
status: 'not_ready',
|
||||||
|
checks: { store: 'unavailable', jobs: 'memory' },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(slices.find((s) => s.key === 'checks-fail')?.count).toBe(1)
|
||||||
|
expect(slices.find((s) => s.key === 'checks-ok')?.count).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns API down slice when health fails', () => {
|
||||||
|
const slices = readinessBreakdown({ status: 'ready', checks: { store: 'ok' } }, false)
|
||||||
|
expect(slices).toHaveLength(1)
|
||||||
|
expect(slices[0]?.key).toBe('health-fail')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,12 +2,65 @@ import type { ReadyStatus } from '@/queries/monitoring'
|
|||||||
|
|
||||||
import type { BreakdownSlice } from './types'
|
import type { BreakdownSlice } from './types'
|
||||||
|
|
||||||
function checkOk(value: boolean | { ok?: boolean; error?: string } | undefined): boolean {
|
/** Значение check из GET /v1/ready (строка, boolean или объект). */
|
||||||
|
export type ReadyCheckValue = boolean | string | { ok?: boolean; error?: string } | null | undefined
|
||||||
|
|
||||||
|
const OK_STRINGS = new Set(['ok', 'ready', 'memory', 'true', 'healthy', 'up'])
|
||||||
|
const FAIL_STRINGS = new Set([
|
||||||
|
'unavailable',
|
||||||
|
'error',
|
||||||
|
'not_ready',
|
||||||
|
'failed',
|
||||||
|
'down',
|
||||||
|
'false',
|
||||||
|
'unhealthy',
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Top-level status GET /v1/ready: API отдаёт `ready`, не `ok`. */
|
||||||
|
export function isSystemReady(status: string | null | undefined): boolean {
|
||||||
|
if (!status) return false
|
||||||
|
const normalized = status.trim().toLowerCase()
|
||||||
|
return normalized === 'ready' || normalized === 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интерпретация check value по контракту handleReady:
|
||||||
|
* store/postgres → "ok" | "unavailable"; jobs → "memory"; store_backend → "memory".
|
||||||
|
*/
|
||||||
|
export function isReadyCheckOk(value: ReadyCheckValue): boolean {
|
||||||
if (typeof value === 'boolean') return value
|
if (typeof value === 'boolean') return value
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase()
|
||||||
|
if (OK_STRINGS.has(normalized)) return true
|
||||||
|
if (FAIL_STRINGS.has(normalized)) return false
|
||||||
|
// неизвестная непустая строка — считать OK (информативный статус бэкенда)
|
||||||
|
return normalized.length > 0
|
||||||
|
}
|
||||||
if (value && typeof value === 'object') return value.ok === true
|
if (value && typeof value === 'object') return value.ok === true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Человекочитаемый статус check для UI. */
|
||||||
|
export function readyCheckStatusLabel(value: ReadyCheckValue, ok: boolean): string {
|
||||||
|
if (!ok) {
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const n = value.trim().toLowerCase()
|
||||||
|
if (n === 'unavailable') return 'Недоступно'
|
||||||
|
if (n === 'not_ready') return 'Не готов'
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object' && value.error) return value.error
|
||||||
|
return 'Ошибка'
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const n = value.trim().toLowerCase()
|
||||||
|
if (n === 'memory') return 'Memory'
|
||||||
|
if (n === 'ok' || n === 'ready' || n === 'healthy' || n === 'true') return 'В норме'
|
||||||
|
if (n) return value
|
||||||
|
}
|
||||||
|
return 'В норме'
|
||||||
|
}
|
||||||
|
|
||||||
export function readinessBreakdown(
|
export function readinessBreakdown(
|
||||||
ready: ReadyStatus | null | undefined,
|
ready: ReadyStatus | null | undefined,
|
||||||
healthOk: boolean,
|
healthOk: boolean,
|
||||||
@@ -28,7 +81,7 @@ export function readinessBreakdown(
|
|||||||
let failCount = 0
|
let failCount = 0
|
||||||
|
|
||||||
for (const value of Object.values(checks)) {
|
for (const value of Object.values(checks)) {
|
||||||
if (checkOk(value)) okCount += 1
|
if (isReadyCheckOk(value)) okCount += 1
|
||||||
else failCount += 1
|
else failCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +114,11 @@ export function readinessBreakdown(
|
|||||||
if (slices.length === 1 && okCount === 0 && failCount === 0) {
|
if (slices.length === 1 && okCount === 0 && failCount === 0) {
|
||||||
slices.push({
|
slices.push({
|
||||||
key: 'ready',
|
key: 'ready',
|
||||||
label: ready?.status === 'ok' ? 'Готов' : 'Ожидает готовности',
|
label: isSystemReady(ready?.status) ? 'Готов' : 'Не готов',
|
||||||
count: 1,
|
count: 1,
|
||||||
color: 'var(--color-chart-4)',
|
color: isSystemReady(ready?.status)
|
||||||
|
? 'var(--color-chart-1)'
|
||||||
|
: 'var(--color-warning)',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export function readyCheckRu(key: string): string {
|
|||||||
return 'Хранилище'
|
return 'Хранилище'
|
||||||
case 'jobs':
|
case 'jobs':
|
||||||
return 'Очередь задач'
|
return 'Очередь задач'
|
||||||
|
case 'store_backend':
|
||||||
|
return 'Бэкенд хранилища'
|
||||||
default:
|
default:
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export interface HealthStatus {
|
|||||||
|
|
||||||
export interface ReadyStatus {
|
export interface ReadyStatus {
|
||||||
status?: string
|
status?: string
|
||||||
checks?: Record<string, boolean | { ok?: boolean; error?: string }>
|
/** Backend: string ("ok"|"memory"|"unavailable"), boolean, or { ok, error }. */
|
||||||
|
checks?: Record<string, boolean | string | { ok?: boolean; error?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function MonitoringComponent() {
|
|||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-2 md:gap-3">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Мониторинг"
|
title="Мониторинг"
|
||||||
description={`Состояние API, BGP и задач для диагностики инцидентов${
|
description={`Состояние API, BGP и задач для диагностики инцидентов${
|
||||||
@@ -110,11 +110,11 @@ function MonitoringComponent() {
|
|||||||
{ value: 'runtime-logs', label: 'Файловые логи' },
|
{ value: 'runtime-logs', label: 'Файловые логи' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TabsContent value="system" className="mt-0 flex flex-col gap-6">
|
<TabsContent value="system" className="mt-0 flex flex-col gap-2 md:gap-3">
|
||||||
{analyticsLoading ? (
|
{analyticsLoading ? (
|
||||||
<AnalyticsDashboardSkeleton />
|
<AnalyticsDashboardSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
|
||||||
<MonitoringHealthCard
|
<MonitoringHealthCard
|
||||||
healthOk={healthQ.data?.ok ?? false}
|
healthOk={healthQ.data?.ok ?? false}
|
||||||
ready={readyQ.data}
|
ready={readyQ.data}
|
||||||
@@ -124,7 +124,7 @@ function MonitoringComponent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
|
||||||
<FrameDataGrid
|
<FrameDataGrid
|
||||||
title="Доступность и готовность"
|
title="Доступность и готовность"
|
||||||
description="GET /v1/health · GET /v1/ready"
|
description="GET /v1/health · GET /v1/ready"
|
||||||
@@ -149,23 +149,24 @@ function MonitoringComponent() {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
description="GET /v1/bird/status"
|
description="GET /v1/bird/status"
|
||||||
contentClassName="py-4"
|
className="h-full"
|
||||||
|
contentClassName="px-5 py-4"
|
||||||
>
|
>
|
||||||
<QueryState
|
<QueryState
|
||||||
data={birdQ.data}
|
data={birdQ.data}
|
||||||
isLoading={birdQ.isLoading}
|
isLoading={birdQ.isLoading}
|
||||||
isError={birdQ.isError}
|
isError={birdQ.isError}
|
||||||
error={birdQ.error}
|
error={birdQ.error}
|
||||||
skeleton={<div className="h-40" />}
|
skeleton={<div className="h-40" />}
|
||||||
onRetry={() => birdQ.refetch()}
|
onRetry={() => birdQ.refetch()}
|
||||||
>
|
>
|
||||||
{(bird) => <BirdSummary bird={bird} />}
|
{(bird) => <BirdSummary bird={bird} />}
|
||||||
</QueryState>
|
</QueryState>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid items-stretch gap-2 md:gap-3 lg:grid-cols-2">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-2 md:gap-3">
|
||||||
<SegmentedProgressCard
|
<SegmentedProgressCard
|
||||||
title="Задачи"
|
title="Задачи"
|
||||||
description="Последние 100 задач · GET /v1/jobs"
|
description="Последние 100 задач · GET /v1/jobs"
|
||||||
@@ -189,8 +190,8 @@ function MonitoringComponent() {
|
|||||||
footer={`В выборке: ${jobs.length} задач`}
|
footer={`В выборке: ${jobs.length} задач`}
|
||||||
/>
|
/>
|
||||||
{failedJobs.length > 0 ? (
|
{failedJobs.length > 0 ? (
|
||||||
<PanelCard title="Последние ошибки" contentClassName="space-y-2 py-4">
|
<PanelCard title="Последние ошибки" contentClassName="px-5 py-4">
|
||||||
<ul className="space-y-2">
|
<ul className="flex flex-col gap-2">
|
||||||
{failedJobs.map((job) => (
|
{failedJobs.map((job) => (
|
||||||
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
|
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -218,28 +219,29 @@ function MonitoringComponent() {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
description="Краткая шпаргалка для первичной диагностики"
|
description="Краткая шпаргалка для первичной диагностики"
|
||||||
contentClassName="py-4"
|
className="h-full"
|
||||||
|
contentClassName="px-5 py-4"
|
||||||
>
|
>
|
||||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
<ul className="flex flex-col gap-3 text-sm text-muted-foreground">
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">API недоступен.</span> Если{' '}
|
<span className="font-medium text-foreground">API недоступен.</span> Если{' '}
|
||||||
<code className="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс API и
|
<code className="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс API и
|
||||||
его логи.
|
его логи.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">Готовность не «Готов».</span> Сначала{' '}
|
<span className="font-medium text-foreground">Готовность не «Готов».</span> Сначала{' '}
|
||||||
<code className="text-xs">postgres</code>, затем <code className="text-xs">store</code>{' '}
|
<code className="text-xs">postgres</code>, затем <code className="text-xs">store</code>{' '}
|
||||||
и <code className="text-xs">jobs</code> в проверках.
|
и <code className="text-xs">jobs</code> в проверках.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">Низкий ratio BGP.</span> Проверьте{' '}
|
<span className="font-medium text-foreground">Низкий ratio BGP.</span> Проверьте{' '}
|
||||||
<code className="text-xs">/v1/bird/status</code>, затем состояние пиров в Сети.
|
<code className="text-xs">/v1/bird/status</code>, затем состояние пиров в Сети.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">Ошибки задач.</span> Откройте Операции и
|
<span className="font-medium text-foreground">Ошибки задач.</span> Откройте Операции и
|
||||||
проверьте последние неуспешные задачи.
|
проверьте последние неуспешные задачи.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -282,7 +284,7 @@ function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
|
|||||||
? Math.round((bird.bgp_established / bird.bgp_sessions_total) * 100)
|
? Math.round((bird.bgp_established / bird.bgp_sessions_total) * 100)
|
||||||
: null
|
: null
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Установлено / всего</span>
|
<span className="text-muted-foreground">Установлено / всего</span>
|
||||||
<span className="font-medium tabular-nums">
|
<span className="font-medium tabular-nums">
|
||||||
|
|||||||
Reference in New Issue
Block a user