Refactored various dashboard components and tab structures to enhance layout and responsiveness. Adjusted class names to include 'min-w-0' for better overflow handling and improved alignment. Updated the ChartDonutMetric to include a tooltip for better data representation. Simplified the DashboardRecentJobsGrid and DashboardRecentRevisionsGrid by replacing DataGridSection with DataGridShell for consistency. Enhanced the OpsDashboard layout for better clarity and usability.
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { Cell, Label, Pie, PieChart } from 'recharts'
|
|
|
|
import {
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
type ChartConfig,
|
|
} from '@evobgp/ui/components/chart'
|
|
import { cn } from '@evobgp/ui/lib/utils'
|
|
|
|
import type { BreakdownSlice } from '@/lib/metrics'
|
|
|
|
export function ChartDonutMetric({
|
|
slices,
|
|
centerLabel,
|
|
centerValue,
|
|
className,
|
|
}: {
|
|
slices: BreakdownSlice[]
|
|
centerLabel: string
|
|
centerValue: string | number
|
|
className?: string
|
|
}) {
|
|
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,
|
|
}))
|
|
|
|
const total = slices.reduce((sum, slice) => sum + slice.count, 0)
|
|
|
|
if (total === 0) {
|
|
return (
|
|
<div className={cn('flex h-48 items-center justify-center text-sm text-muted-foreground', className)}>
|
|
Нет данных
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<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>
|
|
<ChartTooltip content={<ChartTooltipContent nameKey="label" hideLabel />} />
|
|
<Pie
|
|
data={data}
|
|
dataKey="count"
|
|
nameKey="label"
|
|
innerRadius={52}
|
|
outerRadius={72}
|
|
strokeWidth={2}
|
|
stroke="var(--color-card)"
|
|
>
|
|
{data.map((entry) => (
|
|
<Cell key={entry.key} fill={entry.fill} />
|
|
))}
|
|
<Label
|
|
content={({ viewBox }) => {
|
|
if (!viewBox || !('cx' in viewBox) || !('cy' in viewBox)) return null
|
|
const { cx, cy } = viewBox
|
|
return (
|
|
<text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle">
|
|
<tspan x={cx} y={(cy ?? 0) - 6} className="fill-muted-foreground text-xs">
|
|
{centerLabel}
|
|
</tspan>
|
|
<tspan x={cx} y={(cy ?? 0) + 14} className="fill-foreground text-xl font-semibold">
|
|
{centerValue}
|
|
</tspan>
|
|
</text>
|
|
)
|
|
}}
|
|
/>
|
|
</Pie>
|
|
</PieChart>
|
|
</ChartContainer>
|
|
|
|
<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 (
|
|
<li key={slice.key} className="flex items-center justify-between gap-3 text-sm">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span
|
|
className="size-2 shrink-0 rounded-full"
|
|
style={{ backgroundColor: slice.color }}
|
|
/>
|
|
<span className="truncate text-muted-foreground">{slice.label}</span>
|
|
</div>
|
|
<div className="shrink-0 text-right tabular-nums">
|
|
<span className="font-semibold">{slice.count}</span>
|
|
<span className="ml-2 text-muted-foreground">{pct}%</span>
|
|
</div>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|