Исправляет ложные «Ошибки проверок»: GET /v1/ready отдаёт строки ok/memory, а не boolean. Уплотнён ReUI Frame/donut layout на вкладке Система. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import { Cell, Pie, PieChart } from 'recharts'
|
|
|
|
import { PanelCard } from '@/components/panel-card'
|
|
import { Badge } from '@/components/reui/badge'
|
|
import {
|
|
ChartContainer,
|
|
type ChartConfig,
|
|
} from '@evobgp/ui/components/chart'
|
|
import type { BreakdownSlice } from '@/lib/metrics'
|
|
|
|
/** chart-27 / chart-13 inspired donut in PanelCard (Frame surface). */
|
|
export function DonutBreakdownCard({
|
|
title,
|
|
description,
|
|
slices,
|
|
centerLabel,
|
|
badge,
|
|
}: {
|
|
title: string
|
|
description?: string
|
|
slices: BreakdownSlice[]
|
|
centerLabel: string
|
|
badge?: string
|
|
}) {
|
|
const total = slices.reduce((sum, s) => sum + s.count, 0)
|
|
const chartConfig = slices.reduce<ChartConfig>((acc, slice) => {
|
|
acc[slice.key] = { label: slice.label, color: slice.color }
|
|
return acc
|
|
}, {})
|
|
const data = slices.map((slice) => ({ ...slice, fill: slice.color, share: slice.count }))
|
|
|
|
return (
|
|
<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 sm:mx-0">
|
|
<ChartContainer config={chartConfig} className="aspect-square size-36">
|
|
<PieChart>
|
|
<Pie
|
|
data={data}
|
|
dataKey="share"
|
|
nameKey="label"
|
|
innerRadius={48}
|
|
outerRadius={64}
|
|
strokeWidth={2}
|
|
stroke="var(--color-card)"
|
|
>
|
|
{data.map((entry) => (
|
|
<Cell key={entry.key} fill={entry.fill} />
|
|
))}
|
|
</Pie>
|
|
</PieChart>
|
|
</ChartContainer>
|
|
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className="text-muted-foreground text-xs">{centerLabel}</span>
|
|
<span className="text-lg font-semibold tabular-nums">{total}</span>
|
|
</div>
|
|
</div>
|
|
<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-3 text-sm">
|
|
<span className="flex min-w-0 items-center gap-2">
|
|
<span
|
|
className="size-2.5 shrink-0 rounded-full"
|
|
style={{ backgroundColor: slice.color }}
|
|
aria-hidden
|
|
/>
|
|
<span className="truncate">{slice.label}</span>
|
|
</span>
|
|
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
{slice.count} ({pct}%)
|
|
</span>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
</PanelCard>
|
|
)
|
|
}
|