feat(web): enhance certificate management and UI components
Build, Test, and Push CFDM Docker Image / test (push) Successful in 4m0s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 2m17s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

- Updated the CertificatesPage to include a new CertKpiCards component for improved KPI visualization.
- Refactored filter handling and state management for better user experience.
- Introduced new status options and enhanced filtering capabilities in certificates columns.
- Updated the OpsDashboard to utilize KpiStatGrid for displaying key metrics.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-16 22:53:01 +07:00
co-authored by Cursor
parent d44baab8d4
commit 3e326a4509
10 changed files with 436 additions and 116 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics'
export { ResourcePage, type ResourcePageProps, type ResourcePageTab } from './resource-page'
export { OpsDashboard, OpsDashboardHintLink, type OpsKpiCard } from './ops-dashboard'
export { KpiStatGrid, type KpiStatCard, type OpsKpiCard } from './kpi-stat-grid'
export { OpsDashboard, OpsDashboardHintLink } from './ops-dashboard'
export { KanbanBoard, type KanbanBoardProps, type KanbanColumnConfig } from './kanban-board'
export { DetailPanel, type DetailMetricCard } from './detail-panel'
export { SettingsShell, type SettingsTabConfig } from './settings-shell'
@@ -0,0 +1,144 @@
import type { ReactNode } from 'react'
import { Frame, FramePanel } from '@/components/reui/frame'
import { cn } from '@cfdm/ui/lib/utils'
import { Item, ItemMedia } from '@cfdm/ui/components/item'
import { Skeleton } from '@cfdm/ui/components/skeleton'
export interface KpiStatCard {
id: string
typeLabel: string
title: string
metricLabel: string
value: string | number
icon: ReactNode
iconBg?: string
hint?: ReactNode
detail?: ReactNode
badge?: ReactNode
onSelect?: () => void
selected?: boolean
}
/** @deprecated Use KpiStatCard */
export type OpsKpiCard = KpiStatCard
interface KpiStatGridProps {
cards: KpiStatCard[]
isLoading?: boolean
emptyMessage?: ReactNode
emptyIcon?: ReactNode
className?: string
skeletonCount?: number
}
const ICON_TILE_CLASS =
'border-background flex size-10.5 items-center justify-center border-2 p-0 [background-image:radial-gradient(48.05%_48.05%_at_50%_5.95%,rgba(255,255,255,0.4)_0%,rgba(255,255,255,0)_100%)] shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5 [&_svg]:text-white'
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
const interactive = Boolean(card.onSelect)
const Panel = interactive ? 'button' : 'div'
return (
<Panel
type={interactive ? 'button' : undefined}
onClick={card.onSelect}
className={cn(
'text-start',
interactive &&
'hover:bg-muted/40 focus-visible:ring-ring rounded-[calc(var(--frame-radius)-2px)] transition-colors focus-visible:ring-2 focus-visible:outline-none',
card.selected && 'bg-muted/30 ring-primary/30 ring-1',
)}
>
<FramePanel className="flex h-full flex-col items-start gap-4">
<div className="flex w-full items-center gap-2.5">
<Item className={cn('p-0', ICON_TILE_CLASS, card.iconBg ?? 'bg-chart-1')}>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<p className="text-muted-foreground min-w-0 flex-1 text-xs leading-tight">
{card.typeLabel}
</p>
</div>
<div className="space-y-0.5">
<div className="text-foreground text-2xl leading-none font-bold tabular-nums">
{card.value}
</div>
<div className="text-sm leading-tight font-medium">{card.title}</div>
<div className="text-muted-foreground text-xs leading-snug">{card.metricLabel}</div>
</div>
{card.badge || card.hint ? (
<div className="mt-auto flex flex-wrap items-center gap-2">
{card.badge}
{card.hint}
</div>
) : null}
{card.detail ? (
<p className="text-muted-foreground text-xs leading-snug">{card.detail}</p>
) : null}
</FramePanel>
</Panel>
)
}
function KpiStatGridSkeleton({ count }: { count: number }) {
return (
<Frame className="@container w-full">
<div className="grid gap-1 @2xl:grid-cols-2 @5xl:grid-cols-4">
{Array.from({ length: count }).map((_, index) => (
<FramePanel key={index} className="flex flex-col gap-4">
<div className="flex items-center gap-2.5">
<Skeleton className="size-10.5 rounded-lg" />
<Skeleton className="h-3 w-20" />
</div>
<div className="space-y-2">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-5 w-28" />
</FramePanel>
))}
</div>
</Frame>
)
}
export function KpiStatGrid({
cards,
isLoading = false,
emptyMessage,
emptyIcon,
className,
skeletonCount = 4,
}: KpiStatGridProps) {
if (isLoading) {
return <KpiStatGridSkeleton count={skeletonCount} />
}
if (cards.length === 0) {
return (
<Frame dense spacing="sm" className={cn('w-full', className)}>
<FramePanel className="flex items-center gap-3 p-4">
{emptyIcon}
{emptyMessage ? (
<p className="text-muted-foreground text-sm">{emptyMessage}</p>
) : null}
</FramePanel>
</Frame>
)
}
return (
<Frame className={cn('@container w-full', className)}>
<div className="grid gap-1 @2xl:grid-cols-2 @5xl:grid-cols-4">
{cards.map((card) => (
<KpiStatCardItem key={card.id} card={card} />
))}
</div>
</Frame>
)
}
@@ -7,21 +7,10 @@ import {
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { cn } from '@cfdm/ui/lib/utils'
import { debugAgentLog } from '@/lib/debug-agent-log'
import { Item, ItemMedia } from '@cfdm/ui/components/item'
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
export interface OpsKpiCard {
id: string
typeLabel: string
title: string
metricLabel: string
value: string | number
hint?: ReactNode
detail?: ReactNode
icon: ReactNode
iconBg?: string
}
export type OpsKpiCard = KpiStatCard
interface OpsDashboardProps {
title?: string
@@ -31,46 +20,6 @@ interface OpsDashboardProps {
queue: ReactNode
}
function KpiCardItem({ card }: { card: OpsKpiCard }) {
return (
<FramePanel>
<div className="flex items-center gap-2.5">
<Item
className={cn(
'p-0',
'border-background flex size-10 items-center justify-center border-2 [background-image:radial-gradient(48.05%_48.05%_at_50%_5.95%,rgba(255,255,255,0.4)_0%,rgba(255,255,255,0)_100%)] shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5 [&_svg]:text-white',
card.iconBg ?? 'bg-chart-1',
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="flex min-w-0 flex-col gap-1">
<p className="text-muted-foreground text-sm leading-tight">
{card.typeLabel}
</p>
<h3 className="truncate text-sm leading-tight font-medium">{card.title}</h3>
</div>
</div>
<div className="mt-5 flex flex-col gap-1.5">
<p className="text-muted-foreground text-sm leading-tight">
{card.metricLabel}
</p>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xl font-medium tracking-tight tabular-nums">
{card.value}
</span>
{card.hint}
</div>
{card.detail ? (
<p className="text-muted-foreground text-xs leading-snug">{card.detail}</p>
) : null}
</div>
</FramePanel>
)
}
export function OpsDashboard({
title = 'Панель управления',
description = 'Обзор доменов, групп, сервисов и сертификатов Cloudflare',
@@ -108,13 +57,7 @@ export function OpsDashboard({
</header>
<section aria-label="Ключевые метрики">
<Frame className="@container w-full">
<div className="grid gap-1 @2xl:grid-cols-2 @5xl:grid-cols-4">
{kpiCards.map((card) => (
<KpiCardItem key={card.id} card={card} />
))}
</div>
</Frame>
<KpiStatGrid cards={kpiCards} skeletonCount={4} />
</section>
<section