quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 1m6s
CD / quality (push) Successful in 1m21s
CD / publish (push) Successful in 2m36s
Соседние Frame-колонки как в EvoBGP вместо вложенной оболочки; compact empty через IconTile elevated; счётчики ReUI Badge. Co-authored-by: Cursor <[email protected]>
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
|
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
|
|
|
|
export type OpsKpiCard = KpiStatCard
|
|
|
|
interface OpsDashboardProps {
|
|
kpiCards: OpsKpiCard[]
|
|
/** Optional slot under KPI (e.g. QuickActionGrid) */
|
|
afterKpi?: ReactNode
|
|
charts: ReactNode
|
|
queue: ReactNode
|
|
queueTitle?: string
|
|
queueDescription?: string
|
|
isLoading?: boolean
|
|
}
|
|
|
|
/**
|
|
* Ops dashboard: KPI → optional Quick Actions → charts → queue.
|
|
* Queue is a sibling Frame grid — never wrap Frames inside another Frame.
|
|
* @see https://reui.io/preview/base/dashboard-1
|
|
* @see https://reui.io/preview/base/stats-12
|
|
* @see https://reui.io/docs/components/base/frame
|
|
*/
|
|
const rootClassName =
|
|
'text-foreground @container flex w-full flex-col gap-2 md:gap-3'
|
|
|
|
function OpsDashboardSkeleton() {
|
|
return (
|
|
<div className={rootClassName}>
|
|
<KpiStatGrid cards={[]} isLoading skeletonCount={4} />
|
|
<div className="grid gap-2 @3xl:grid-cols-2">
|
|
<Skeleton className="h-64 w-full rounded-xl" />
|
|
<Skeleton className="h-64 w-full rounded-xl" />
|
|
</div>
|
|
<div className="grid gap-2 @3xl:grid-cols-3">
|
|
<Skeleton className="h-40 w-full rounded-xl" />
|
|
<Skeleton className="h-40 w-full rounded-xl" />
|
|
<Skeleton className="h-40 w-full rounded-xl" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function OpsDashboard({
|
|
kpiCards,
|
|
afterKpi,
|
|
charts,
|
|
queue,
|
|
queueTitle = 'Требуют внимания',
|
|
queueDescription = 'Проблемы health-check, истекающие сертификаты и домены без группы',
|
|
isLoading = false,
|
|
}: OpsDashboardProps) {
|
|
if (isLoading) {
|
|
return <OpsDashboardSkeleton />
|
|
}
|
|
|
|
return (
|
|
<div className={rootClassName}>
|
|
<section aria-label="Ключевые метрики">
|
|
<KpiStatGrid cards={kpiCards} skeletonCount={4} />
|
|
</section>
|
|
|
|
{afterKpi}
|
|
|
|
<section
|
|
aria-label="Аналитика"
|
|
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
|
|
>
|
|
{charts}
|
|
</section>
|
|
|
|
<section aria-label={queueTitle} className="flex min-w-0 flex-col gap-2">
|
|
<div className="flex min-w-0 flex-col gap-1">
|
|
<h2 className="text-sm font-semibold tracking-tight">{queueTitle}</h2>
|
|
{queueDescription ? (
|
|
<p className="text-muted-foreground max-w-prose text-sm">
|
|
{queueDescription}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
{queue}
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|