Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m25s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m49s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
- Updated AppSwitcherEditor to include item separators for better visual organization. - Improved VpsTrackerIntegrationCard with badges indicating synchronization status and token configuration. - Refactored SettingsShell to streamline layout and enhance accessibility with improved navigation. - Added detail fields to KPI cards in DashboardPage for clearer information presentation. - Introduced new components in the ReUI kit for better integration and user experience. Co-authored-by: Cursor <[email protected]>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { Link } from '@tanstack/react-router'
|
|
import {
|
|
Frame,
|
|
FrameDescription,
|
|
FrameHeader,
|
|
FramePanel,
|
|
FrameTitle,
|
|
} from '@/components/reui/frame'
|
|
import { cn } from '@cfdm/ui/lib/utils'
|
|
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
|
|
|
export interface OpsKpiCard {
|
|
id: string
|
|
typeLabel: string
|
|
title: string
|
|
metricLabel: string
|
|
value: string | number
|
|
hint?: ReactNode
|
|
detail?: ReactNode
|
|
icon: ReactNode
|
|
iconBg?: string
|
|
}
|
|
|
|
interface OpsDashboardProps {
|
|
title?: string
|
|
description?: string
|
|
kpiCards: OpsKpiCard[]
|
|
charts: ReactNode
|
|
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',
|
|
kpiCards,
|
|
charts,
|
|
queue,
|
|
}: OpsDashboardProps) {
|
|
return (
|
|
<div className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-4 md:gap-6">
|
|
<header className="px-1">
|
|
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
<p className="text-muted-foreground max-w-2xl text-sm leading-relaxed">
|
|
{description}
|
|
</p>
|
|
</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>
|
|
</section>
|
|
|
|
<section
|
|
aria-label="Аналитика"
|
|
className="grid min-w-0 auto-rows-fr items-start gap-4 @3xl:grid-cols-2"
|
|
>
|
|
{charts}
|
|
</section>
|
|
|
|
<section aria-label="Требуют внимания">
|
|
<Frame dense spacing="sm" className="w-full">
|
|
<FrameHeader>
|
|
<FrameTitle>Требуют внимания</FrameTitle>
|
|
<FrameDescription>
|
|
Истекающие сертификаты и домены без группы
|
|
</FrameDescription>
|
|
</FrameHeader>
|
|
<FramePanel>{queue}</FramePanel>
|
|
</Frame>
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function OpsDashboardHintLink({
|
|
to,
|
|
search,
|
|
children,
|
|
}: {
|
|
to: string
|
|
search?: Record<string, unknown>
|
|
children: ReactNode
|
|
}) {
|
|
return (
|
|
<Link
|
|
to={to}
|
|
search={search}
|
|
className="text-primary text-sm font-medium hover:underline"
|
|
>
|
|
{children}
|
|
</Link>
|
|
)
|
|
}
|