CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 52s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m7s
Refactored multiple dashboard components to utilize the new PanelCard for better organization and presentation. Updated the DashboardFramePanel, DashboardQuickLinks, and DashboardOperationsBreakdown components to streamline layouts and enhance user experience. Removed deprecated components and improved loading states in various sections, ensuring a cohesive interface throughout the application.
93 lines
3.3 KiB
TypeScript
93 lines
3.3 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. */
|
|
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">
|
|
<div className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center">
|
|
{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">
|
|
<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="min-w-0 flex-1 space-y-2">
|
|
{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">
|
|
<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>
|
|
</>
|
|
)}
|
|
{badge ? (
|
|
<Badge variant="success-light" className="absolute top-4 right-4 hidden sm:flex">
|
|
{badge}
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
</PanelCard>
|
|
)
|
|
}
|