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
- 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]>
204 lines
7.4 KiB
TypeScript
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>
|
|
)
|
|
}
|