feat(ui): добавить поддержку варианта для KPI карточек
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m10s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m46s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m10s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m46s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 5s
Обновлены компоненты CertKpiCards, DomainKpiCards, GroupKpiCards и ServiceKpiCards для поддержки новых вариантов отображения ('warning' и 'destructive') в зависимости от состояния данных. Также обновлен интерфейс KpiStatCard для включения свойства variant. Обновлены ссылки в документации для отражения изменений в дизайне KPI карточек.
This commit is contained in:
@@ -75,6 +75,7 @@ export function CertKpiCards({
|
||||
hint: 'Истекают скоро',
|
||||
icon: <AlertTriangleIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
variant: warningCount > 0 ? 'warning' : 'default',
|
||||
onSelect: () => onSelectTab('warning'),
|
||||
selected: activeTab === 'warning',
|
||||
},
|
||||
@@ -85,6 +86,7 @@ export function CertKpiCards({
|
||||
hint: 'Ошибка или истёк',
|
||||
icon: <ShieldAlertIcon aria-hidden />,
|
||||
iconClassName: 'text-destructive',
|
||||
variant: problemCount > 0 ? 'destructive' : 'default',
|
||||
onSelect: () => onSelectTab('expired'),
|
||||
selected: activeTab === 'expired',
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ export function DomainKpiCards({
|
||||
hint: 'Не назначена группа',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
variant: withoutGroup > 0 ? 'warning' : 'default',
|
||||
onSelect: () => onSelectTab('without_group'),
|
||||
selected: activeTab === 'without_group',
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@ export function GroupKpiCards({
|
||||
hint: 'Без доменов',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground',
|
||||
variant: empty > 0 ? 'warning' : 'default',
|
||||
onSelect: () => onSelectTab('empty'),
|
||||
selected: activeTab === 'empty',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
|
||||
export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics'
|
||||
export { ResourcePage, type ResourcePageProps, type ResourcePageTab } from './resource-page'
|
||||
export { KpiStatGrid, type KpiStatCard, type OpsKpiCard } from './kpi-stat-grid'
|
||||
export { KpiStatGrid, type KpiStatCard, type KpiStatVariant, type OpsKpiCard } from './kpi-stat-grid'
|
||||
export { OpsDashboard } from './ops-dashboard'
|
||||
export { KanbanBoard, KanbanBoardSkeleton, type KanbanBoardProps, type KanbanColumnConfig } from './kanban-board'
|
||||
export { DetailPanel, type DetailMetricCard } from './detail-panel'
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
|
||||
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
||||
|
||||
export interface KpiStatCard {
|
||||
id: string
|
||||
label: string
|
||||
@@ -16,6 +19,8 @@ export interface KpiStatCard {
|
||||
selected?: boolean
|
||||
icon?: ReactNode
|
||||
iconClassName?: string
|
||||
/** Semantic color for the primary value */
|
||||
variant?: KpiStatVariant
|
||||
footer?: ReactNode
|
||||
}
|
||||
|
||||
@@ -33,16 +38,40 @@ interface KpiStatGridProps {
|
||||
|
||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||
|
||||
/** KPI body — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
const VALUE_VARIANT_CLASS: Record<KpiStatVariant, string> = {
|
||||
default: 'text-foreground',
|
||||
warning: 'text-warning',
|
||||
destructive: 'text-destructive',
|
||||
}
|
||||
|
||||
function kpiCols(count: number): string {
|
||||
if (count <= 1) return 'grid-cols-1'
|
||||
if (count === 2) return 'grid-cols-1 @xl:grid-cols-2'
|
||||
if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3'
|
||||
if (count === 4) return 'grid-cols-1 @3xl:grid-cols-2 @6xl:grid-cols-4'
|
||||
if (count === 5) return 'grid-cols-2 @3xl:grid-cols-3 xl:grid-cols-5'
|
||||
if (count === 6) return 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-6'
|
||||
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
||||
}
|
||||
|
||||
function resolveFooter(card: KpiStatCard): ReactNode {
|
||||
if (card.footer) return card.footer
|
||||
if (card.hint) {
|
||||
return (
|
||||
<Badge variant="outline" size="sm">
|
||||
{card.hint}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
const footerContent =
|
||||
card.footer ??
|
||||
(card.hint ? (
|
||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
||||
) : null)
|
||||
const footer = resolveFooter(card)
|
||||
const valueVariant = card.variant ?? 'default'
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-full flex-col items-start gap-4">
|
||||
<div className="relative z-10 flex h-full flex-col items-start gap-3">
|
||||
{card.icon ? (
|
||||
<Item
|
||||
className={cn(
|
||||
@@ -57,13 +86,18 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
|
||||
<span
|
||||
className={cn(
|
||||
'text-2xl leading-none font-bold tabular-nums',
|
||||
VALUE_VARIANT_CLASS[valueVariant],
|
||||
)}
|
||||
>
|
||||
{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}
|
||||
{footer ? <div className="mt-auto w-full">{footer}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -74,17 +108,13 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||
'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',
|
||||
'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2',
|
||||
)
|
||||
|
||||
if (card.to) {
|
||||
return (
|
||||
<FramePanel className={panelClass}>
|
||||
<Link
|
||||
to={card.to}
|
||||
search={card.search}
|
||||
className="focus-visible:outline-none"
|
||||
>
|
||||
<Link to={card.to} search={card.search} className="focus-visible:outline-none">
|
||||
<KpiStatCardBody card={card} />
|
||||
</Link>
|
||||
</FramePanel>
|
||||
@@ -115,15 +145,13 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||
function KpiStatGridSkeleton({ count }: { count: number }) {
|
||||
return (
|
||||
<Frame className="@container w-full">
|
||||
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
||||
<div className={cn('grid gap-2', kpiCols(count))}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<FramePanel key={index} className="flex flex-col items-start gap-4">
|
||||
<FramePanel key={index} className="flex flex-col gap-3">
|
||||
<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" />
|
||||
<Skeleton className="h-7 w-14" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="mt-auto h-4.5 w-16 rounded-full" />
|
||||
</FramePanel>
|
||||
))}
|
||||
</div>
|
||||
@@ -131,6 +159,10 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hybrid KPI grid — compact stats-12 Frame strip.
|
||||
* Preview: https://reui.io/preview/base/stats-12
|
||||
*/
|
||||
export function KpiStatGrid({
|
||||
cards,
|
||||
isLoading = false,
|
||||
@@ -158,7 +190,7 @@ export function KpiStatGrid({
|
||||
|
||||
return (
|
||||
<Frame className={cn('@container w-full', className)}>
|
||||
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
||||
<div className={cn('grid gap-2', kpiCols(cards.length))}>
|
||||
{cards.map((card) => (
|
||||
<KpiStatCardItem key={card.id} card={card} />
|
||||
))}
|
||||
|
||||
@@ -66,6 +66,7 @@ export function ServiceKpiCards({
|
||||
hint: down > 0 ? `Down: ${down}` : 'Не в группе',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
variant: ungrouped > 0 || down > 0 ? 'warning' : 'default',
|
||||
onSelect: () => onSelectTab('ungrouped'),
|
||||
selected: activeTab === 'ungrouped',
|
||||
},
|
||||
|
||||
@@ -178,6 +178,7 @@ function DashboardPage() {
|
||||
: 'Все в группах',
|
||||
icon: <GlobeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
variant: ungroupedCount > 0 ? 'warning' : 'default',
|
||||
to: '/domains',
|
||||
},
|
||||
{
|
||||
@@ -200,6 +201,7 @@ function DashboardPage() {
|
||||
: undefined,
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
variant: attentionCount > 0 ? 'destructive' : ungroupedServiceCount > 0 ? 'warning' : 'default',
|
||||
to: '/services',
|
||||
search: { domainId: undefined },
|
||||
},
|
||||
@@ -213,6 +215,7 @@ function DashboardPage() {
|
||||
: `${certOk} в норме`,
|
||||
icon: <ShieldCheckIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
variant: certWarnings > 0 ? 'warning' : 'default',
|
||||
to: '/certificates',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
|
||||
| Зона | Block | Preview |
|
||||
|------|-------|---------|
|
||||
| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 |
|
||||
| KPI | `stats-12` (primary); `card-35` compact strip | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-35 |
|
||||
| KPI | hybrid `stats-12` (compact Frame strip: colored icon → value ± variant → label → Badge footer) | https://reui.io/preview/base/stats-12 |
|
||||
| Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 |
|
||||
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||
| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 |
|
||||
@@ -31,7 +31,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
|
||||
| `KpiStatGrid` | stats-12 KPI tiles |
|
||||
| `KpiStatGrid` | hybrid compact stats-12 KPI tiles (`variant`, Badge footer) |
|
||||
| `OpsDashboard` | KPI + charts + attention queue |
|
||||
| `SettingsShell` | settings nav + Outlet |
|
||||
| `DetailPanel` | detail Frame sections |
|
||||
|
||||
Reference in New Issue
Block a user