feat(ui): обновить KPI с card-35 на stats-12 во всех компонентах
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m27s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m52s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Failing after 6s
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m27s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m52s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Failing after 6s
Заменены ссылки на блоки card-35 на stats-12 в документации и компонентах, включая KpiStatGrid, CertKpiCards, DomainKpiCards, GroupKpiCards и ServiceKpiCards. Удален неиспользуемый компонент AppCard. Добавлены иконки для улучшения визуализации KPI. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@cfdm/ui/components/card'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
|
||||
export type AppCardProps = ComponentProps<typeof Card>
|
||||
export type AppCardHeaderProps = ComponentProps<typeof CardHeader>
|
||||
export type AppCardTitleProps = ComponentProps<typeof CardTitle>
|
||||
export type AppCardDescriptionProps = ComponentProps<typeof CardDescription>
|
||||
export type AppCardActionProps = ComponentProps<typeof CardAction>
|
||||
export type AppCardContentProps = ComponentProps<typeof CardContent>
|
||||
export type AppCardFooterProps = ComponentProps<typeof CardFooter>
|
||||
|
||||
export function AppCard({ className, ...props }: AppCardProps) {
|
||||
return <Card className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardHeader({ className, ...props }: AppCardHeaderProps) {
|
||||
return <CardHeader className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardTitle({ className, ...props }: AppCardTitleProps) {
|
||||
return <CardTitle className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardDescription({
|
||||
className,
|
||||
...props
|
||||
}: AppCardDescriptionProps) {
|
||||
return <CardDescription className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardAction({ className, ...props }: AppCardActionProps) {
|
||||
return <CardAction className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardContent({ className, ...props }: AppCardContentProps) {
|
||||
return <CardContent className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardFooter({ className, ...props }: AppCardFooterProps) {
|
||||
return <CardFooter className={cn(className)} {...props} />
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ShieldCheckIcon } from 'lucide-react'
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CheckCircle2Icon,
|
||||
ShieldAlertIcon,
|
||||
ShieldCheckIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
|
||||
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
|
||||
@@ -28,6 +33,7 @@ interface CertKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function CertKpiCards({
|
||||
summary,
|
||||
total,
|
||||
@@ -47,6 +53,8 @@ export function CertKpiCards({
|
||||
label: 'Всего',
|
||||
value: allCount,
|
||||
hint: 'Проверенные хосты',
|
||||
icon: <ShieldCheckIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -55,6 +63,8 @@ export function CertKpiCards({
|
||||
label: 'OK',
|
||||
value: okCount,
|
||||
hint: 'Активные сертификаты',
|
||||
icon: <CheckCircle2Icon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('active'),
|
||||
selected: activeTab === 'active',
|
||||
},
|
||||
@@ -63,6 +73,8 @@ export function CertKpiCards({
|
||||
label: 'Предупреждение',
|
||||
value: warningCount,
|
||||
hint: 'Истекают скоро',
|
||||
icon: <AlertTriangleIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('warning'),
|
||||
selected: activeTab === 'warning',
|
||||
},
|
||||
@@ -71,6 +83,8 @@ export function CertKpiCards({
|
||||
label: 'Проблемы',
|
||||
value: problemCount,
|
||||
hint: 'Ошибка или истёк',
|
||||
icon: <ShieldAlertIcon aria-hidden />,
|
||||
iconClassName: 'text-destructive',
|
||||
onSelect: () => onSelectTab('expired'),
|
||||
selected: activeTab === 'expired',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { GlobeIcon } from 'lucide-react'
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
FolderOpenIcon,
|
||||
GlobeIcon,
|
||||
ServerIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
import type { DomainListItem } from '@/lib/schemas'
|
||||
|
||||
@@ -10,7 +15,7 @@ interface DomainKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function DomainKpiCards({
|
||||
domains,
|
||||
activeTab,
|
||||
@@ -29,6 +34,8 @@ export function DomainKpiCards({
|
||||
label: 'Всего',
|
||||
value: list.length,
|
||||
hint: 'Импортированные зоны',
|
||||
icon: <GlobeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -37,6 +44,8 @@ export function DomainKpiCards({
|
||||
label: 'Без группы',
|
||||
value: withoutGroup,
|
||||
hint: 'Не назначена группа',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('without_group'),
|
||||
selected: activeTab === 'without_group',
|
||||
},
|
||||
@@ -45,6 +54,8 @@ export function DomainKpiCards({
|
||||
label: 'Active',
|
||||
value: active,
|
||||
hint: 'Статус Cloudflare',
|
||||
icon: <CheckCircle2Icon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: false,
|
||||
},
|
||||
@@ -53,6 +64,8 @@ export function DomainKpiCards({
|
||||
label: 'С сервисами',
|
||||
value: withServices,
|
||||
hint: 'Есть привязки',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { FolderTreeIcon } from 'lucide-react'
|
||||
import { FolderOpenIcon, FolderTreeIcon, FoldersIcon } from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
import type { GroupCatalogRow } from '@/components/columns/groups-columns'
|
||||
|
||||
@@ -10,7 +10,7 @@ interface GroupKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function GroupKpiCards({
|
||||
rows,
|
||||
activeTab,
|
||||
@@ -27,6 +27,8 @@ export function GroupKpiCards({
|
||||
label: 'Всего',
|
||||
value: rows.length,
|
||||
hint: 'Группы доменов',
|
||||
icon: <FolderTreeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -35,6 +37,8 @@ export function GroupKpiCards({
|
||||
label: 'С доменами',
|
||||
value: withDomains,
|
||||
hint: 'Непустые группы',
|
||||
icon: <FoldersIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('with_domains'),
|
||||
selected: activeTab === 'with_domains',
|
||||
},
|
||||
@@ -43,6 +47,8 @@ export function GroupKpiCards({
|
||||
label: 'Пустые',
|
||||
value: empty,
|
||||
hint: 'Без доменов',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground',
|
||||
onSelect: () => onSelectTab('empty'),
|
||||
selected: activeTab === 'empty',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
|
||||
@@ -13,6 +14,9 @@ export interface KpiStatCard {
|
||||
search?: Record<string, unknown>
|
||||
onSelect?: () => void
|
||||
selected?: boolean
|
||||
icon?: ReactNode
|
||||
iconClassName?: string
|
||||
footer?: ReactNode
|
||||
}
|
||||
|
||||
/** @deprecated Use KpiStatCard */
|
||||
@@ -27,16 +31,39 @@ interface KpiStatGridProps {
|
||||
skeletonCount?: number
|
||||
}
|
||||
|
||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||
|
||||
/** KPI body — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
const footerContent =
|
||||
card.footer ??
|
||||
(card.hint ? (
|
||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
||||
) : null)
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
<span className="text-muted-foreground text-sm">{card.label}</span>
|
||||
<span className="text-foreground text-xl leading-tight font-semibold tabular-nums">
|
||||
{card.value}
|
||||
</span>
|
||||
{card.hint ? (
|
||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
||||
<div className="relative z-10 flex h-full flex-col items-start gap-4">
|
||||
{card.icon ? (
|
||||
<Item
|
||||
className={cn(
|
||||
'border-background bg-muted flex size-10.5 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
||||
card.iconClassName ?? DEFAULT_ICON_CLASS,
|
||||
)}
|
||||
>
|
||||
<ItemMedia variant="icon" className="size-auto">
|
||||
{card.icon}
|
||||
</ItemMedia>
|
||||
</Item>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
|
||||
{card.value}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
|
||||
</div>
|
||||
|
||||
{footerContent ? <div className="mt-auto w-full">{footerContent}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,13 +71,12 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||
const interactive = Boolean(card.to || card.onSelect)
|
||||
const panelClass = cn(
|
||||
'relative isolate flex flex-col gap-3',
|
||||
'relative isolate flex h-full flex-col',
|
||||
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
||||
interactive &&
|
||||
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
|
||||
)
|
||||
|
||||
// card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели
|
||||
if (card.to) {
|
||||
return (
|
||||
<FramePanel className={panelClass}>
|
||||
@@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
|
||||
<Frame className="@container w-full">
|
||||
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<FramePanel key={index} className="flex flex-col gap-3">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-7 w-14" />
|
||||
<FramePanel key={index} className="flex flex-col items-start gap-4">
|
||||
<Skeleton className="size-10.5 rounded-lg" />
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<Skeleton className="h-7 w-14" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-28" />
|
||||
</FramePanel>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ServerIcon } from 'lucide-react'
|
||||
import {
|
||||
FolderOpenIcon,
|
||||
PowerIcon,
|
||||
PowerOffIcon,
|
||||
ServerIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
import type { ServiceCatalogRow } from '@/components/columns/services-columns'
|
||||
|
||||
@@ -10,7 +15,7 @@ interface ServiceKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function ServiceKpiCards({
|
||||
rows,
|
||||
activeTab,
|
||||
@@ -29,6 +34,8 @@ export function ServiceKpiCards({
|
||||
label: 'Всего',
|
||||
value: rows.length,
|
||||
hint: 'Сервисы',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -37,6 +44,8 @@ export function ServiceKpiCards({
|
||||
label: 'Включены',
|
||||
value: enabled,
|
||||
hint: 'Активные',
|
||||
icon: <PowerIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('enabled'),
|
||||
selected: activeTab === 'enabled',
|
||||
},
|
||||
@@ -45,6 +54,8 @@ export function ServiceKpiCards({
|
||||
label: 'Выключены',
|
||||
value: disabled,
|
||||
hint: 'Отключены',
|
||||
icon: <PowerOffIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground',
|
||||
onSelect: () => onSelectTab('disabled'),
|
||||
selected: activeTab === 'disabled',
|
||||
},
|
||||
@@ -53,6 +64,8 @@ export function ServiceKpiCards({
|
||||
label: 'Без группы',
|
||||
value: ungrouped,
|
||||
hint: down > 0 ? `Down: ${down}` : 'Не в группе',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('ungrouped'),
|
||||
selected: activeTab === 'ungrouped',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user