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 <cursoragent@cursor.com>
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
}
/** Page-level section header — etalon EvoBGP. */
/** Page-level section header — etalon EvoBGP / Domains list. */
export function PageHeader({ title, description, actions }: PageHeaderProps) {
return (
<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>
{description ? (
<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 { StatusBadge } from '@/components/status-badge'
import { EmptyState } from '@/components/empty-state'
import {
Frame,
FrameDescription,
@@ -85,9 +86,15 @@ export function CertStatusChart({ data }: CertStatusChartProps) {
<FrameTitle>Статусы сертификатов</FrameTitle>
<FrameDescription>Распределение по последней проверке</FrameDescription>
</FrameHeader>
<FramePanel fit>
<FramePanel fit className="flex min-h-52 flex-col">
{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}
@@ -167,9 +174,15 @@ export function GroupDomainsChart({ data }: GroupDomainsChartProps) {
<FrameTitle>Домены по группам</FrameTitle>
<FrameDescription>Топ-6 групп по количеству зон</FrameDescription>
</FrameHeader>
<FramePanel fit>
<FramePanel fit className="flex min-h-52 flex-col">
{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">
<ChartContainer
@@ -7,14 +7,11 @@ import {
FrameTitle,
} from '@/components/reui/frame'
import { Skeleton } from '@cfdm/ui/components/skeleton'
import { PageHeader } from '@/components/page-header'
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
export type OpsKpiCard = KpiStatCard
interface OpsDashboardProps {
title?: string
description?: string
kpiCards: OpsKpiCard[]
/** Optional slot under KPI (e.g. QuickActionGrid) */
afterKpi?: ReactNode
@@ -23,16 +20,13 @@ interface OpsDashboardProps {
isLoading?: boolean
}
/** Denser stack for KPI / charts / queue. PageHeader lives outside via PageShell. */
const rootClassName =
'text-foreground @container flex w-full flex-col gap-2 md:gap-3'
function OpsDashboardSkeleton() {
return (
<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} />
<div className="grid gap-2 @3xl:grid-cols-2">
<Skeleton className="h-64 w-full rounded-xl" />
@@ -44,8 +38,6 @@ function OpsDashboardSkeleton() {
}
export function OpsDashboard({
title = 'Панель управления',
description = 'Обзор доменов, групп, сервисов и сертификатов Cloudflare',
kpiCards,
afterKpi,
charts,
@@ -58,8 +50,6 @@ export function OpsDashboard({
return (
<div className={rootClassName}>
<PageHeader title={title} description={description} />
<section aria-label="Ключевые метрики">
<KpiStatGrid cards={kpiCards} skeletonCount={4} />
</section>
+30 -7
View File
@@ -18,6 +18,8 @@ import {
serviceGroupsQueryOptions,
} from '@/queries'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { EmptyState } from '@/components/empty-state'
import {
CertStatusChart,
GroupDomainsChart,
@@ -276,6 +278,10 @@ function DashboardPage() {
return (
<PageShell>
<PageHeader
title="Панель управления"
description="Обзор доменов, групп, сервисов и сертификатов Cloudflare"
/>
<OpsDashboard
isLoading={isLoading}
kpiCards={kpiCards}
@@ -309,9 +315,14 @@ function DashboardPage() {
) : null}
</div>
{attentionServices.length === 0 ? (
<p className="text-muted-foreground text-sm">
Нет сервисов со статусом Down или Slow
</p>
<div className="flex min-h-24 items-center justify-center py-4">
<EmptyState
title="Нет проблем"
description="Нет сервисов со статусом Down или Slow"
centered={false}
stackedIcon={false}
/>
</div>
) : (
<ItemGroup className="gap-2">
{attentionServices.map((service) => (
@@ -356,9 +367,14 @@ function DashboardPage() {
) : null}
</div>
{expiringCerts.length === 0 ? (
<p className="text-muted-foreground text-sm">
В пределах 14 дней истечений нет
</p>
<div className="flex min-h-24 items-center justify-center py-4">
<EmptyState
title="Нет истечений"
description="В пределах 14 дней истечений нет"
centered={false}
stackedIcon={false}
/>
</div>
) : (
<ItemGroup className="gap-2">
{expiringCerts.map(({ cert, ts }) => (
@@ -391,7 +407,14 @@ function DashboardPage() {
) : null}
</div>
{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">
{ungroupedDomains.map((domain) => (