fix(ui): выровнять ритм Dashboard с list-страницами
PageHeader вынесен из OpsDashboard; empty чартов/очереди — EmptyState по центру. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user