CD / update-wiki (push) Successful in 5s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 52s
CD / quality (push) Successful in 1m3s
CD / publish (push) Successful in 1m38s
Светлая тема и сайдбар совпадают с артбордом, показатели собраны в одну рамку с цветом статуса. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
import { useMemo } from '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'])
|
|
const PROBLEM_STATUSES = new Set(['expired', 'error', 'conflict'])
|
|
|
|
function countFromSummary(
|
|
summary: [string, number][] | undefined,
|
|
statuses: Set<string>,
|
|
): number {
|
|
if (!summary) return 0
|
|
return summary
|
|
.filter(([status]) => statuses.has(status))
|
|
.reduce((sum, [, count]) => sum + count, 0)
|
|
}
|
|
|
|
function totalFromSummary(summary: [string, number][] | undefined): number {
|
|
if (!summary) return 0
|
|
return summary.reduce((sum, [, count]) => sum + count, 0)
|
|
}
|
|
|
|
interface CertKpiStatsProps {
|
|
summary?: [string, number][]
|
|
total?: number
|
|
activeTab: string
|
|
isLoading?: boolean
|
|
onSelectTab: (tabId: string) => void
|
|
}
|
|
|
|
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
|
export function CertKpiStats({
|
|
summary,
|
|
total,
|
|
activeTab,
|
|
isLoading,
|
|
onSelectTab,
|
|
}: CertKpiStatsProps) {
|
|
const cards = useMemo<KpiStatCard[]>(() => {
|
|
const allCount = total ?? totalFromSummary(summary)
|
|
const okCount = countFromSummary(summary, ACTIVE_STATUSES)
|
|
const warningCount = countFromSummary(summary, new Set(['warning']))
|
|
const problemCount = countFromSummary(summary, PROBLEM_STATUSES)
|
|
|
|
return [
|
|
{
|
|
id: 'all',
|
|
label: 'Всего',
|
|
value: allCount,
|
|
hint: 'Проверенные хосты',
|
|
icon: <ShieldCheckIcon aria-hidden />,
|
|
iconClassName: 'text-info',
|
|
onSelect: () => onSelectTab('all'),
|
|
selected: activeTab === 'all',
|
|
},
|
|
{
|
|
id: 'active',
|
|
label: 'OK',
|
|
value: okCount,
|
|
hint: 'Активные сертификаты',
|
|
icon: <CheckCircle2Icon aria-hidden />,
|
|
iconClassName: 'text-success',
|
|
onSelect: () => onSelectTab('active'),
|
|
selected: activeTab === 'active',
|
|
},
|
|
{
|
|
id: 'warning',
|
|
label: 'Предупреждение',
|
|
value: warningCount,
|
|
hint: 'Истекают скоро',
|
|
icon: <AlertTriangleIcon aria-hidden />,
|
|
iconClassName: 'text-warning',
|
|
variant: warningCount > 0 ? 'warning' : 'default',
|
|
onSelect: () => onSelectTab('warning'),
|
|
selected: activeTab === 'warning',
|
|
},
|
|
{
|
|
id: 'expired',
|
|
label: 'Проблемы',
|
|
value: problemCount,
|
|
hint: 'Ошибка или истёк',
|
|
icon: <ShieldAlertIcon aria-hidden />,
|
|
iconClassName: 'text-destructive',
|
|
variant: problemCount > 0 ? 'destructive' : 'default',
|
|
onSelect: () => onSelectTab('expired'),
|
|
selected: activeTab === 'expired',
|
|
},
|
|
]
|
|
}, [summary, total, activeTab, onSelectTab])
|
|
|
|
return (
|
|
<KpiStatGrid
|
|
cards={cards}
|
|
isLoading={isLoading}
|
|
skeletonCount={4}
|
|
/>
|
|
)
|
|
}
|