Files
cloudflare-domain-manager/apps/web/src/components/reui-kit/dashboard-analytics.tsx
T
DenozordecandCursor 1e9cc04a59
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m9s
quality / api (push) Successful in 40s
CD / quality (push) Successful in 2m0s
CD / publish (push) Successful in 2m38s
feat(api): enhance health check and DNS management
- Added batch endpoint for health status queries to reduce the number of requests.
- Improved DNS record management with caching and invalidation mechanisms.
- Updated health check service to handle new configurations and improve performance.
- Refactored certificate service to utilize concurrency for checks, enhancing efficiency.
- Removed unused dependencies and optimized package configurations.

Co-authored-by: Cursor <[email protected]>
2026-09-03 16:16:41 +07:00

204 lines
7.4 KiB
TypeScript

import { useMemo, useRef } from 'react'
import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis } from 'recharts'
import { StatusBadge } from '@/components/status-badge'
import { EmptyState } from '@/components/empty-state'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@cfdm/ui/components/chart'
import { Separator } from '@cfdm/ui/components/separator'
import { cn } from '@cfdm/ui/lib/utils'
const statusChartConfig = {
count: { label: 'Сертификаты' },
active: { label: 'Активен', color: 'var(--success)' },
ok: { label: 'OK', color: 'var(--success)' },
warning: { label: 'Предупреждение', color: 'var(--warning)' },
pending_push: { label: 'Ожидает', color: 'var(--info)' },
expired: { label: 'Истёк', color: 'var(--destructive)' },
error: { label: 'Ошибка', color: 'var(--destructive)' },
unknown: { label: 'Неизвестно', color: 'var(--muted-foreground)' },
} satisfies ChartConfig
const groupChartConfig = {
count: { label: 'Домены', color: 'var(--chart-1)' },
} satisfies ChartConfig
function statusColor(status: string) {
return (
(statusChartConfig as Record<string, { color?: string }>)[status]?.color ??
'var(--chart-1)'
)
}
interface CertStatusChartProps {
data: { status: string; count: number }[]
}
export function CertStatusChart({ data }: CertStatusChartProps) {
const chartRef = useRef<HTMLDivElement>(null)
const total = useMemo(
() => data.reduce((sum, entry) => sum + entry.count, 0),
[data],
)
return (
<Frame dense spacing="sm" className="w-full">
<FrameHeader>
<FrameTitle>Статусы сертификатов</FrameTitle>
<FrameDescription>Распределение по последней проверке</FrameDescription>
</FrameHeader>
<FramePanel fit className="flex min-h-52 flex-col">
{data.length === 0 ? (
<div className="flex min-h-52 w-full flex-1 items-center justify-center py-6">
<EmptyState
title="Нет данных о сертификатах"
description="Запустите проверку на странице сертификатов"
centered={false}
/>
</div>
) : (
<div
ref={chartRef}
className="grid w-full gap-6 @md:grid-cols-[9rem_minmax(0,1fr)] @md:items-center"
>
<div className="relative mx-auto size-36 shrink-0">
<ChartContainer
config={statusChartConfig}
className="aspect-square size-36"
initialDimension={{ width: 144, height: 144 }}
>
<PieChart margin={{ top: 4, right: 4, bottom: 4, left: 4 }}>
<ChartTooltip
content={<ChartTooltipContent nameKey="status" hideLabel />}
/>
<Pie
data={data}
dataKey="count"
nameKey="status"
innerRadius={44}
outerRadius={64}
strokeWidth={2}
stroke="var(--background)"
>
{data.map((entry) => (
<Cell key={entry.status} fill={statusColor(entry.status)} />
))}
</Pie>
</PieChart>
</ChartContainer>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 flex items-center justify-center"
>
<div className="flex flex-col items-center">
<span className="text-muted-foreground text-xs">Всего</span>
<span className="text-lg font-semibold tabular-nums">{total}</span>
</div>
</div>
</div>
<ul className="flex min-w-0 flex-col">
{data.map((entry, index) => (
<li key={entry.status}>
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<span
aria-hidden="true"
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: statusColor(entry.status) }}
/>
<StatusBadge status={entry.status} />
</div>
<span className="text-sm font-medium tabular-nums">{entry.count}</span>
</div>
{index < data.length - 1 ? <Separator /> : null}
</li>
))}
</ul>
</div>
)}
</FramePanel>
</Frame>
)
}
interface GroupDomainsChartProps {
data: { name: string; count: number }[]
}
export function GroupDomainsChart({ data }: GroupDomainsChartProps) {
const chartRef = useRef<HTMLDivElement>(null)
return (
<Frame dense spacing="sm" className="w-full">
<FrameHeader>
<FrameTitle>Домены по группам</FrameTitle>
<FrameDescription>Топ-6 групп по количеству зон</FrameDescription>
</FrameHeader>
<FramePanel fit className="flex min-h-52 flex-col">
{data.length === 0 ? (
<div className="flex min-h-52 w-full flex-1 items-center justify-center py-6">
<EmptyState
title="Нет групп с доменами"
description="Назначьте домены в группы"
centered={false}
/>
</div>
) : (
<div ref={chartRef} className="flex w-full flex-col gap-4">
<ChartContainer
config={groupChartConfig}
className="aspect-auto h-52 w-full min-h-52"
initialDimension={{ width: 480, height: 208 }}
>
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tickMargin={8}
interval={0}
height={40}
tickFormatter={(value: string) =>
value.length > 12 ? `${value.slice(0, 11)}…` : value
}
/>
<ChartTooltip content={<ChartTooltipContent nameKey="count" />} />
<Bar dataKey="count" fill="var(--color-count)" radius={4} />
</BarChart>
</ChartContainer>
<ul className="grid gap-2 @sm:grid-cols-2">
{data.map((entry) => (
<li
key={entry.name}
className={cn(
'bg-muted/40 flex items-center justify-between gap-2 rounded-lg border px-3 py-2',
)}
>
<span className="truncate text-sm font-medium">{entry.name}</span>
<span className="text-muted-foreground shrink-0 text-sm tabular-nums">
{entry.count}
</span>
</li>
))}
</ul>
</div>
)}
</FramePanel>
</Frame>
)
}