fix(ui): выровнять ритм Dashboard с list-страницами
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m47s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 5s

PageHeader вынесен из OpsDashboard; empty чартов/очереди — EmptyState по центру.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-18 20:47:28 +07:00
co-authored by Cursor
parent 0a4293b735
commit f1767b8a1f
4 changed files with 50 additions and 24 deletions
+2 -2
View File
@@ -6,11 +6,11 @@ interface PageHeaderProps {
actions?: ReactNode actions?: ReactNode
} }
/** Page-level section header — etalon EvoBGP. */ /** Page-level section header — etalon EvoBGP / Domains list. */
export function PageHeader({ title, description, actions }: PageHeaderProps) { export function PageHeader({ title, description, actions }: PageHeaderProps) {
return ( return (
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-px">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1> <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{description ? ( {description ? (
<p className="text-muted-foreground text-sm">{description}</p> <p className="text-muted-foreground text-sm">{description}</p>
@@ -2,6 +2,7 @@ import { useMemo, useEffect, useRef } from 'react'
import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis } from 'recharts' import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis } from 'recharts'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { EmptyState } from '@/components/empty-state'
import { import {
Frame, Frame,
FrameDescription, FrameDescription,
@@ -85,9 +86,15 @@ export function CertStatusChart({ data }: CertStatusChartProps) {
<FrameTitle>Статусы сертификатов</FrameTitle> <FrameTitle>Статусы сертификатов</FrameTitle>
<FrameDescription>Распределение по последней проверке</FrameDescription> <FrameDescription>Распределение по последней проверке</FrameDescription>
</FrameHeader> </FrameHeader>
<FramePanel fit> <FramePanel fit className="flex min-h-52 flex-col">
{data.length === 0 ? ( {data.length === 0 ? (
<p className="text-muted-foreground text-sm">Нет данных о сертификатах</p> <div className="flex min-h-52 w-full flex-1 items-center justify-center py-6">
<EmptyState
title="Нет данных о сертификатах"
description="Запустите проверку на странице сертификатов"
centered={false}
/>
</div>
) : ( ) : (
<div <div
ref={chartRef} ref={chartRef}
@@ -167,9 +174,15 @@ export function GroupDomainsChart({ data }: GroupDomainsChartProps) {
<FrameTitle>Домены по группам</FrameTitle> <FrameTitle>Домены по группам</FrameTitle>
<FrameDescription>Топ-6 групп по количеству зон</FrameDescription> <FrameDescription>Топ-6 групп по количеству зон</FrameDescription>
</FrameHeader> </FrameHeader>
<FramePanel fit> <FramePanel fit className="flex min-h-52 flex-col">
{data.length === 0 ? ( {data.length === 0 ? (
<p className="text-muted-foreground text-sm">Нет групп с доменами</p> <div className="flex min-h-52 w-full flex-1 items-center justify-center py-6">
<EmptyState
title="Нет групп с доменами"
description="Назначьте домены в группы"
centered={false}
/>
</div>
) : ( ) : (
<div ref={chartRef} className="flex w-full flex-col gap-4"> <div ref={chartRef} className="flex w-full flex-col gap-4">
<ChartContainer <ChartContainer
@@ -7,14 +7,11 @@ import {
FrameTitle, FrameTitle,
} from '@/components/reui/frame' } from '@/components/reui/frame'
import { Skeleton } from '@cfdm/ui/components/skeleton' import { Skeleton } from '@cfdm/ui/components/skeleton'
import { PageHeader } from '@/components/page-header'
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid' import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
export type OpsKpiCard = KpiStatCard export type OpsKpiCard = KpiStatCard
interface OpsDashboardProps { interface OpsDashboardProps {
title?: string
description?: string
kpiCards: OpsKpiCard[] kpiCards: OpsKpiCard[]
/** Optional slot under KPI (e.g. QuickActionGrid) */ /** Optional slot under KPI (e.g. QuickActionGrid) */
afterKpi?: ReactNode afterKpi?: ReactNode
@@ -23,16 +20,13 @@ interface OpsDashboardProps {
isLoading?: boolean isLoading?: boolean
} }
/** Denser stack for KPI / charts / queue. PageHeader lives outside via PageShell. */
const rootClassName = const rootClassName =
'text-foreground @container flex w-full flex-col gap-2 md:gap-3' 'text-foreground @container flex w-full flex-col gap-2 md:gap-3'
function OpsDashboardSkeleton() { function OpsDashboardSkeleton() {
return ( return (
<div className={rootClassName}> <div className={rootClassName}>
<header className="px-1">
<Skeleton className="h-7 w-56" />
<Skeleton className="mt-2 h-4 w-80 max-w-full" />
</header>
<KpiStatGrid cards={[]} isLoading skeletonCount={4} /> <KpiStatGrid cards={[]} isLoading skeletonCount={4} />
<div className="grid gap-2 @3xl:grid-cols-2"> <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" />
@@ -44,8 +38,6 @@ function OpsDashboardSkeleton() {
} }
export function OpsDashboard({ export function OpsDashboard({
title = 'Панель управления',
description = 'Обзор доменов, групп, сервисов и сертификатов Cloudflare',
kpiCards, kpiCards,
afterKpi, afterKpi,
charts, charts,
@@ -58,8 +50,6 @@ export function OpsDashboard({
return ( return (
<div className={rootClassName}> <div className={rootClassName}>
<PageHeader title={title} description={description} />
<section aria-label="Ключевые метрики"> <section aria-label="Ключевые метрики">
<KpiStatGrid cards={kpiCards} skeletonCount={4} /> <KpiStatGrid cards={kpiCards} skeletonCount={4} />
</section> </section>
+30 -7
View File
@@ -18,6 +18,8 @@ import {
serviceGroupsQueryOptions, serviceGroupsQueryOptions,
} from '@/queries' } from '@/queries'
import { PageShell } from '@/components/page-shell' import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { EmptyState } from '@/components/empty-state'
import { import {
CertStatusChart, CertStatusChart,
GroupDomainsChart, GroupDomainsChart,
@@ -276,6 +278,10 @@ function DashboardPage() {
return ( return (
<PageShell> <PageShell>
<PageHeader
title="Панель управления"
description="Обзор доменов, групп, сервисов и сертификатов Cloudflare"
/>
<OpsDashboard <OpsDashboard
isLoading={isLoading} isLoading={isLoading}
kpiCards={kpiCards} kpiCards={kpiCards}
@@ -309,9 +315,14 @@ function DashboardPage() {
) : null} ) : null}
</div> </div>
{attentionServices.length === 0 ? ( {attentionServices.length === 0 ? (
<p className="text-muted-foreground text-sm"> <div className="flex min-h-24 items-center justify-center py-4">
Нет сервисов со статусом Down или Slow <EmptyState
</p> title="Нет проблем"
description="Нет сервисов со статусом Down или Slow"
centered={false}
stackedIcon={false}
/>
</div>
) : ( ) : (
<ItemGroup className="gap-2"> <ItemGroup className="gap-2">
{attentionServices.map((service) => ( {attentionServices.map((service) => (
@@ -356,9 +367,14 @@ function DashboardPage() {
) : null} ) : null}
</div> </div>
{expiringCerts.length === 0 ? ( {expiringCerts.length === 0 ? (
<p className="text-muted-foreground text-sm"> <div className="flex min-h-24 items-center justify-center py-4">
В пределах 14 дней истечений нет <EmptyState
</p> title="Нет истечений"
description="В пределах 14 дней истечений нет"
centered={false}
stackedIcon={false}
/>
</div>
) : ( ) : (
<ItemGroup className="gap-2"> <ItemGroup className="gap-2">
{expiringCerts.map(({ cert, ts }) => ( {expiringCerts.map(({ cert, ts }) => (
@@ -391,7 +407,14 @@ function DashboardPage() {
) : null} ) : null}
</div> </div>
{ungroupedDomains.length === 0 ? ( {ungroupedDomains.length === 0 ? (
<p className="text-muted-foreground text-sm">Все домены в группах</p> <div className="flex min-h-24 items-center justify-center py-4">
<EmptyState
title="Всё сгруппировано"
description="Все домены в группах"
centered={false}
stackedIcon={false}
/>
</div>
) : ( ) : (
<ItemGroup className="gap-2"> <ItemGroup className="gap-2">
{ungroupedDomains.map((domain) => ( {ungroupedDomains.map((domain) => (