feat(web): align ops screens with ReUI PRO kit and Frame surface
OpsDashboard+afterKpi на dashboard; FrameDataGrid вместо DataGridCard; KpiStatGrid вместо SectionCards; SettingsShell без Separator. Preview: dashboard-1, stats-12, data-grid-filtering-2, settings-16. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -4,7 +4,7 @@ import { Plus, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
|
||||
import { AccessApiKeysGrid } from '@/components/access/access-api-keys-grid'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { ApiKeyCreateDialog } from '@/components/access/api-key-create-dialog'
|
||||
import { ApiKeyTokenDialog } from '@/components/access/api-key-token-dialog'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
@@ -47,7 +47,7 @@ export function AccessApiKeysCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="API-ключи"
|
||||
description="Управление ключами tenant. Полный токен показывается только при создании и ротации."
|
||||
actions={
|
||||
@@ -85,7 +85,7 @@ export function AccessApiKeysCard({
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
|
||||
<ApiKeyCreateDialog
|
||||
open={createOpen}
|
||||
|
||||
@@ -136,6 +136,25 @@ function buildKpis({
|
||||
]
|
||||
}
|
||||
|
||||
export function buildDashboardKpiCards({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
return buildKpis({ modules, peers, speakers, jobs, loading }).map((card) => ({
|
||||
...card,
|
||||
id: card.id ?? String(card.label),
|
||||
}))
|
||||
}
|
||||
|
||||
export function DashboardKpiGrid({
|
||||
modules,
|
||||
peers,
|
||||
|
||||
@@ -5,10 +5,10 @@ import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
import { DataGridToolbar } from '@/components/data-grid-toolbar'
|
||||
import {
|
||||
PanelCard,
|
||||
panelCardContentFlushClassName,
|
||||
panelCardFooterClassName,
|
||||
} from '@/components/panel-card'
|
||||
import { FrameDataGrid } from '@/components/reui-kit/frame-data-grid'
|
||||
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
|
||||
import { DataGrid, DataGridContainer } from '@/components/reui/data-grid/data-grid'
|
||||
import { DataGridTable } from '@/components/reui/data-grid/data-grid-table'
|
||||
@@ -65,25 +65,29 @@ export function DataGridShell<TData extends object>({
|
||||
)
|
||||
}
|
||||
|
||||
interface DataGridCardProps {
|
||||
title?: string
|
||||
description?: string
|
||||
/** @deprecated Prefer FrameDataGrid from @/components/reui-kit */
|
||||
export function DataGridCard({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title?: ReactNode
|
||||
description?: ReactNode
|
||||
actions?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DataGridCard({ title, description, actions, children, className }: DataGridCardProps) {
|
||||
}) {
|
||||
return (
|
||||
<PanelCard
|
||||
<FrameDataGrid
|
||||
title={title}
|
||||
description={description}
|
||||
actions={actions}
|
||||
className={className}
|
||||
contentClassName={panelCardContentFlushClassName}
|
||||
>
|
||||
{children}
|
||||
</PanelCard>
|
||||
<div className={panelCardContentFlushClassName}>{children}</div>
|
||||
</FrameDataGrid>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
import { CategoryBadge } from '@/components/category-badge'
|
||||
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
|
||||
import { DataGridCard, DataGridSection } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { DataGridSection } from '@/components/data-grid-shell'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
|
||||
@@ -112,7 +113,7 @@ export function LookupMatchesGrid({
|
||||
})
|
||||
|
||||
return (
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Совпадения"
|
||||
description="Entries и snapshots · клик по строке открывает модуль"
|
||||
>
|
||||
@@ -128,6 +129,6 @@ export function LookupMatchesGrid({
|
||||
void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } })
|
||||
}
|
||||
/>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from 'sonner'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
import { ModuleEntriesGrid, type ModuleEntryDeleteTarget } from '@/components/modules/module-entries-grid'
|
||||
@@ -128,7 +128,7 @@ export function ModuleEntriesSection({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
actions={
|
||||
@@ -166,7 +166,7 @@ export function ModuleEntriesSection({
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
|
||||
{mod.type === 'DOMAINS' ? (
|
||||
<ModuleDomainEntryDialog
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { OperationsJobsGrid } from '@/components/operations/operations-jobs-grid'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
@@ -51,7 +51,7 @@ export function OperationsJobsCard({
|
||||
const filtered = useMemo(() => filterJobs(jobs, tab), [jobs, tab])
|
||||
|
||||
return (
|
||||
<DataGridCard title="Задачи" description="Фильтр по статусу · data-grid-filtering pattern">
|
||||
<FrameDataGrid title="Задачи" description="Фильтр по статусу · data-grid-filtering pattern">
|
||||
<div className="px-5 pt-3">
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as JobTab)} className="w-full">
|
||||
<TabsList variant="line" className="w-full justify-start gap-6">
|
||||
@@ -81,6 +81,6 @@ export function OperationsJobsCard({
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Frame,
|
||||
FrameDescription,
|
||||
FrameHeader,
|
||||
FramePanel,
|
||||
FrameTitle,
|
||||
} from '@/components/reui/frame'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
/**
|
||||
* Frame shell for list/grid sections (replaces DataGridCard Card-named API).
|
||||
* @see https://reui.io/preview/base/data-grid-filtering-2
|
||||
* @see https://reui.io/docs/components/base/frame
|
||||
*/
|
||||
export function FrameDataGrid({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title?: ReactNode
|
||||
description?: ReactNode
|
||||
actions?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
const hasHeader = Boolean(title || description || actions)
|
||||
return (
|
||||
<Frame dense spacing="sm" className={cn('w-full gap-0 p-0', className)}>
|
||||
<FramePanel className="flex flex-col gap-0 p-0 shadow-xs">
|
||||
{hasHeader ? (
|
||||
<FrameHeader className="flex-row items-start justify-between gap-3 border-b">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-px">
|
||||
{title ? <FrameTitle>{title}</FrameTitle> : null}
|
||||
{description ? (
|
||||
<FrameDescription>{description}</FrameDescription>
|
||||
) : null}
|
||||
</div>
|
||||
{actions ? (
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</FrameHeader>
|
||||
) : null}
|
||||
<div className="p-0">{children}</div>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
)
|
||||
}
|
||||
@@ -22,5 +22,6 @@ export {
|
||||
} from './kpi-stat-grid'
|
||||
export { QuickActionGrid, type QuickActionItem } from './quick-action-grid'
|
||||
export { OpsDashboard } from './ops-dashboard'
|
||||
export { FrameDataGrid } from './frame-data-grid'
|
||||
export { DetailPanel, type DetailMetricCard } from './detail-panel'
|
||||
export { SettingsShell, type SettingsTabConfig } from './settings-shell'
|
||||
|
||||
@@ -16,10 +16,13 @@ interface OpsDashboardProps {
|
||||
title?: string
|
||||
description?: string
|
||||
kpiCards: OpsKpiCard[]
|
||||
/** Slot after KPI (QuickActionGrid). Preview: stats-12 · card-12 */
|
||||
afterKpi?: ReactNode
|
||||
charts: ReactNode
|
||||
queue: ReactNode
|
||||
queueTitle?: string
|
||||
queueDescription?: string
|
||||
headerActions?: ReactNode
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
@@ -43,14 +46,21 @@ function OpsDashboardSkeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ops dashboard: KPI → optional Quick Actions → charts → attention queue.
|
||||
* @see https://reui.io/preview/base/dashboard-1
|
||||
* @see https://reui.io/preview/base/stats-12
|
||||
*/
|
||||
export function OpsDashboard({
|
||||
title = 'Панель управления',
|
||||
description = 'Обзор модулей, сети, задач и состояния плоскости управления',
|
||||
kpiCards,
|
||||
afterKpi,
|
||||
charts,
|
||||
queue,
|
||||
queueTitle = 'Требуют внимания',
|
||||
queueDescription = 'Проблемы BGP, сбои задач и офлайн-спикеры',
|
||||
headerActions,
|
||||
isLoading = false,
|
||||
}: OpsDashboardProps) {
|
||||
if (isLoading) {
|
||||
@@ -59,12 +69,14 @@ export function OpsDashboard({
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<PageHeader title={title} description={description} />
|
||||
<PageHeader title={title} description={description} actions={headerActions} />
|
||||
|
||||
<section aria-label="Ключевые метрики">
|
||||
<KpiStatGrid cards={kpiCards} skeletonCount={4} />
|
||||
</section>
|
||||
|
||||
{afterKpi ? <section aria-label="Быстрые действия">{afterKpi}</section> : null}
|
||||
|
||||
<section
|
||||
aria-label="Аналитика"
|
||||
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useIsMobile } from '@evobgp/ui/hooks/use-mobile'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import { PageShell } from '@/components/page-shell'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { Separator } from '@evobgp/ui/components/separator'
|
||||
|
||||
export interface SettingsTabConfig {
|
||||
id: string
|
||||
@@ -37,6 +36,11 @@ interface SettingsShellProps {
|
||||
tabs?: SettingsTabConfig[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings shell — Frame surface, no hairline Separator under header.
|
||||
* @see https://reui.io/preview/base/settings-16
|
||||
* @see https://reui.io/preview/base/settings-3
|
||||
*/
|
||||
export function SettingsShell({
|
||||
title = 'Настройки',
|
||||
description = 'Подключение UI и параметры плоскости управления',
|
||||
@@ -48,10 +52,7 @@ export function SettingsShell({
|
||||
return (
|
||||
<PageShell>
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<PageHeader title={title} description={description} />
|
||||
<Separator />
|
||||
</div>
|
||||
<PageHeader title={title} description={description} />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||
@@ -45,7 +45,7 @@ export function ScheduleJobsCard({
|
||||
const filtered = useMemo(() => filterJobs(jobs, tab), [jobs, tab])
|
||||
|
||||
return (
|
||||
<DataGridCard title="Задачи" description="Последние задачи из API">
|
||||
<FrameDataGrid title="Задачи" description="Последние задачи из API">
|
||||
<div className="px-5 pt-3">
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as JobTab)} className="w-full">
|
||||
<TabsList variant="line" className="w-full justify-start gap-6">
|
||||
@@ -72,6 +72,6 @@ export function ScheduleJobsCard({
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { KpiStatGrid, type KpiStatItem } from '@/components/kpi-stat-grid'
|
||||
|
||||
export interface SectionCardItem {
|
||||
label: ReactNode
|
||||
value: string | number | ReactElement
|
||||
hint?: ReactNode
|
||||
icon?: ReactNode
|
||||
badge?: ReactNode
|
||||
variant?: 'default' | 'warning' | 'destructive'
|
||||
active?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const VARIANT_ICON_CLASS: Record<NonNullable<SectionCardItem['variant']>, string> = {
|
||||
default: 'text-muted-foreground',
|
||||
warning: 'text-warning',
|
||||
destructive: 'text-destructive',
|
||||
}
|
||||
|
||||
function hintToFooter(hint: ReactNode) {
|
||||
if (typeof hint === 'string') {
|
||||
return (
|
||||
<Badge variant="outline" size="sm">
|
||||
{hint}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return hint
|
||||
}
|
||||
|
||||
function toKpiStatItem(item: SectionCardItem, index: number): KpiStatItem {
|
||||
const variant = item.variant ?? 'default'
|
||||
const footer =
|
||||
item.badge ?? (item.hint ? hintToFooter(item.hint) : undefined)
|
||||
|
||||
return {
|
||||
id: typeof item.label === 'string' ? item.label : `section-${index}`,
|
||||
icon: item.icon,
|
||||
iconClassName: VARIANT_ICON_CLASS[variant],
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
variant,
|
||||
footer,
|
||||
active: item.active,
|
||||
onClick: item.onClick,
|
||||
}
|
||||
}
|
||||
|
||||
/** KPI row for route sections — ReUI stats-12 / dashboard PRO pattern. */
|
||||
export function SectionCards({ items, className }: { items: SectionCardItem[]; className?: string }) {
|
||||
return (
|
||||
<KpiStatGrid
|
||||
items={items.map(toKpiStatItem)}
|
||||
className={className}
|
||||
aria-label="Показатели"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { PanelCard } from '@/components/panel-card'
|
||||
import { AccessApiKeysCard } from '@/components/access/access-api-keys-card'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { SectionCards, type SectionCardItem } from '@/components/section-cards'
|
||||
import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
|
||||
import { SectionCardsSkeleton } from '@/components/skeletons'
|
||||
import { sessionCanManageApiKeys } from '@/lib/auth'
|
||||
import { authSessionQueryOptions } from '@/queries/auth'
|
||||
@@ -35,13 +35,13 @@ function AccessComponent() {
|
||||
|
||||
const refreshing = sessionQuery.isFetching || keysQuery.isFetching
|
||||
|
||||
const kpiItems: SectionCardItem[] = useMemo(
|
||||
const kpiItems: KpiStatItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Всего ключей',
|
||||
value: keys.length,
|
||||
icon: <KeyRound className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="primary-light" size="sm">
|
||||
в tenant
|
||||
</Badge>
|
||||
@@ -51,7 +51,7 @@ function AccessComponent() {
|
||||
label: 'Активных',
|
||||
value: activeCount,
|
||||
icon: <ShieldCheck className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="success-light" size="sm">
|
||||
не отозваны
|
||||
</Badge>
|
||||
@@ -62,7 +62,7 @@ function AccessComponent() {
|
||||
value: revokedCount,
|
||||
icon: <ShieldOff className="size-4" />,
|
||||
variant: revokedCount > 0 ? 'warning' : 'default',
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant={revokedCount > 0 ? 'warning-light' : 'outline'} size="sm">
|
||||
revoked
|
||||
</Badge>
|
||||
@@ -156,7 +156,7 @@ function AccessComponent() {
|
||||
{keysQuery.isLoading ? (
|
||||
<SectionCardsSkeleton count={3} />
|
||||
) : (
|
||||
<SectionCards items={kpiItems} />
|
||||
<KpiStatGrid items={kpiItems} />
|
||||
)}
|
||||
<AccessApiKeysCard
|
||||
items={keys}
|
||||
|
||||
@@ -8,16 +8,14 @@ import { Skeleton } from '@evobgp/ui/components/skeleton'
|
||||
|
||||
import { DashboardActivityTimeline } from '@/components/dashboard/dashboard-activity-timeline'
|
||||
import { DashboardFramePanel } from '@/components/dashboard/dashboard-frame-panel'
|
||||
import { DashboardKpiGrid } from '@/components/dashboard/dashboard-kpi-grid'
|
||||
import { buildDashboardKpiCards } from '@/components/dashboard/dashboard-kpi-grid'
|
||||
import { DashboardModulesGrid } from '@/components/dashboard/dashboard-modules-grid'
|
||||
import { DashboardNetworkHealth } from '@/components/dashboard/dashboard-network-health'
|
||||
import { DashboardOperationsBreakdown } from '@/components/dashboard/dashboard-operations-breakdown'
|
||||
import { DashboardQuickLinks } from '@/components/dashboard/dashboard-quick-links'
|
||||
import { DashboardRecentJobsGrid } from '@/components/dashboard/dashboard-recent-jobs-grid'
|
||||
import { DashboardRecentRevisionsGrid } from '@/components/dashboard/dashboard-recent-revisions-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { AnalyticsDashboardSkeleton } from '@/components/skeletons'
|
||||
|
||||
import { OpsDashboard } from '@/components/reui-kit'
|
||||
import { chartPanelGridClassName, dashboardMainSidebarClassName } from '@/lib/ui-surface'
|
||||
import {
|
||||
moduleNameById,
|
||||
@@ -38,6 +36,11 @@ function parseShowQuickActions(value: unknown): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard — OpsDashboard kit (KPI → QuickActions → charts → queue).
|
||||
* @see https://reui.io/preview/base/dashboard-1
|
||||
* @see https://reui.io/preview/base/stats-12
|
||||
*/
|
||||
function DashboardComponent() {
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
||||
const settingsQ = useQuery(settingsQueryOptions())
|
||||
@@ -74,39 +77,28 @@ function DashboardComponent() {
|
||||
const jobs = jobsQ.data?.items ?? []
|
||||
const nameById = moduleNameById(modules)
|
||||
|
||||
const activityLoading =
|
||||
refreshing && jobs.length === 0 && revisions.length === 0
|
||||
const activityLoading = refreshing && jobs.length === 0 && revisions.length === 0
|
||||
const kpiCards = buildDashboardKpiCards({ modules, peers, speakers, jobs })
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<PageHeader
|
||||
title="Обзор"
|
||||
description={
|
||||
lastUpdated
|
||||
? `Состояние панели управления EvoBGP. Обновлено: ${lastUpdated.toLocaleTimeString('ru-RU')}`
|
||||
: 'Состояние панели управления EvoBGP.'
|
||||
}
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
||||
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
||||
Обновить
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{initialLoading ? (
|
||||
<AnalyticsDashboardSkeleton />
|
||||
) : (
|
||||
<OpsDashboard
|
||||
title="Обзор"
|
||||
description={
|
||||
lastUpdated
|
||||
? `Состояние панели управления EvoBGP. Обновлено: ${lastUpdated.toLocaleTimeString('ru-RU')}`
|
||||
: 'Состояние панели управления EvoBGP.'
|
||||
}
|
||||
headerActions={
|
||||
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
||||
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
||||
Обновить
|
||||
</Button>
|
||||
}
|
||||
kpiCards={kpiCards}
|
||||
isLoading={initialLoading}
|
||||
afterKpi={showQuickActions ? <DashboardQuickLinks /> : null}
|
||||
charts={
|
||||
<>
|
||||
<DashboardKpiGrid
|
||||
modules={modules}
|
||||
peers={peers}
|
||||
speakers={speakers}
|
||||
jobs={jobs}
|
||||
/>
|
||||
|
||||
{showQuickActions ? <DashboardQuickLinks /> : null}
|
||||
|
||||
<div className={dashboardMainSidebarClassName}>
|
||||
<div className="xl:col-span-8">
|
||||
<DashboardModulesGrid modules={modules} isLoading={refreshing} />
|
||||
@@ -120,39 +112,40 @@ function DashboardComponent() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={chartPanelGridClassName}>
|
||||
<DashboardNetworkHealth peers={peers} speakers={speakers} jobs={jobs} />
|
||||
<DashboardOperationsBreakdown jobs={jobs} modules={modules} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<DashboardFramePanel
|
||||
title="Недавние задачи"
|
||||
description="Последние фоновые операции"
|
||||
className="h-full"
|
||||
>
|
||||
{activityLoading ? (
|
||||
<Skeleton className="m-4 h-24 w-auto" />
|
||||
) : (
|
||||
<DashboardRecentJobsGrid jobs={jobs.slice(0, 10)} nameById={nameById} isLoading={refreshing} />
|
||||
)}
|
||||
</DashboardFramePanel>
|
||||
|
||||
<DashboardFramePanel
|
||||
title="Последние ревизии"
|
||||
description="История конфигураций"
|
||||
className="h-full"
|
||||
>
|
||||
{activityLoading ? (
|
||||
<Skeleton className="m-4 h-24 w-auto" />
|
||||
) : (
|
||||
<DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} />
|
||||
)}
|
||||
</DashboardFramePanel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
queueTitle="Недавняя активность"
|
||||
queueDescription="Задачи и ревизии плоскости управления"
|
||||
queue={
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<DashboardFramePanel
|
||||
title="Недавние задачи"
|
||||
description="Последние фоновые операции"
|
||||
className="h-full"
|
||||
>
|
||||
{activityLoading ? (
|
||||
<Skeleton className="m-4 h-24 w-auto" />
|
||||
) : (
|
||||
<DashboardRecentJobsGrid jobs={jobs.slice(0, 10)} nameById={nameById} isLoading={refreshing} />
|
||||
)}
|
||||
</DashboardFramePanel>
|
||||
<DashboardFramePanel
|
||||
title="Последние ревизии"
|
||||
description="История конфигураций"
|
||||
className="h-full"
|
||||
>
|
||||
{activityLoading ? (
|
||||
<Skeleton className="m-4 h-24 w-auto" />
|
||||
) : (
|
||||
<DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} />
|
||||
)}
|
||||
</DashboardFramePanel>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@ import { BookText, Globe, RefreshCw, Tags } from 'lucide-react'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { DirectoriesCommunitiesGrid } from '@/components/directories/directories-communities-grid'
|
||||
import { DirectoriesDohGrid } from '@/components/directories/directories-doh-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { SectionCards, type SectionCardItem } from '@/components/section-cards'
|
||||
import { FrameDataGrid, KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
|
||||
import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons'
|
||||
import { directoriesCommunitiesQueryOptions, directoriesDohQueryOptions } from '@/queries/directories'
|
||||
|
||||
@@ -26,12 +21,12 @@ function DirectoriesComponent() {
|
||||
const dohProfiles = dohQ.data?.items ?? []
|
||||
const loading = communitiesQ.isLoading || dohQ.isLoading
|
||||
|
||||
const items: SectionCardItem[] = [
|
||||
const items: KpiStatItem[] = [
|
||||
{
|
||||
label: 'Сообщества BGP',
|
||||
value: communities.length,
|
||||
icon: <Tags className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="primary-light" size="sm">
|
||||
теги префиксов
|
||||
</Badge>
|
||||
@@ -41,7 +36,7 @@ function DirectoriesComponent() {
|
||||
label: 'DoH профили',
|
||||
value: dohProfiles.length,
|
||||
icon: <Globe className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="info-light" size="sm">
|
||||
резолвинг доменов
|
||||
</Badge>
|
||||
@@ -51,7 +46,7 @@ function DirectoriesComponent() {
|
||||
label: 'Справочники',
|
||||
value: 'Общие',
|
||||
icon: <BookText className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="outline" size="sm">
|
||||
все модули tenant
|
||||
</Badge>
|
||||
@@ -80,7 +75,7 @@ function DirectoriesComponent() {
|
||||
}
|
||||
/>
|
||||
|
||||
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
||||
{loading ? <SectionCardsSkeleton count={3} /> : <KpiStatGrid items={items} />}
|
||||
|
||||
<BadgeTabs
|
||||
defaultValue="communities"
|
||||
@@ -90,7 +85,7 @@ function DirectoriesComponent() {
|
||||
]}
|
||||
>
|
||||
<TabsContent value="communities" className="mt-0">
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Сообщества BGP"
|
||||
description="Теги для префиксов в фильтрах BIRD"
|
||||
>
|
||||
@@ -111,11 +106,11 @@ function DirectoriesComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="doh" className="mt-0">
|
||||
<DataGridCard title="DoH профили" description="Резолверы DNS-over-HTTPS для доменных модулей">
|
||||
<FrameDataGrid title="DoH профили" description="Резолверы DNS-over-HTTPS для доменных модулей">
|
||||
<QueryState
|
||||
data={dohProfiles}
|
||||
isLoading={dohQ.isLoading}
|
||||
@@ -133,7 +128,7 @@ function DirectoriesComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
</TabsContent>
|
||||
</BadgeTabs>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Plus, RefreshCw } from 'lucide-react'
|
||||
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { ProjectsEmptyState } from '@/components/patterns/projects-empty-state'
|
||||
import { ModulesListGrid } from '@/components/modules/modules-list-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
@@ -39,7 +39,7 @@ function ModulesListComponent() {
|
||||
}
|
||||
/>
|
||||
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Все модули"
|
||||
actions={
|
||||
<Button size="sm" render={<Link to="/modules/new" />}>
|
||||
@@ -65,7 +65,7 @@ function ModulesListComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AlertTriangle, Bird, Database, FileText, RefreshCw } from 'lucide-react
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { PanelCard } from '@/components/panel-card'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { jobKindRu } from '@/lib/ui-labels'
|
||||
import {
|
||||
@@ -125,7 +125,7 @@ function MonitoringComponent() {
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Доступность и готовность"
|
||||
description="GET /v1/health · GET /v1/ready"
|
||||
>
|
||||
@@ -139,7 +139,7 @@ function MonitoringComponent() {
|
||||
>
|
||||
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
|
||||
<PanelCard
|
||||
title={
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState, useMemo } from 'react'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { PanelCard } from '@/components/panel-card'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { OperationsAnalyticsCard } from '@/components/analytics'
|
||||
import { SelectMenu } from '@/components/select-field'
|
||||
import { OperationsJobsCard } from '@/components/operations/operations-jobs-card'
|
||||
@@ -136,7 +136,7 @@ function OperationsComponent() {
|
||||
]}
|
||||
>
|
||||
<TabsContent value="revisions" className="mt-0">
|
||||
<DataGridCard title="История ревизий">
|
||||
<FrameDataGrid title="История ревизий">
|
||||
<QueryState
|
||||
data={revisions}
|
||||
isLoading={revisionsQ.isLoading}
|
||||
@@ -154,7 +154,7 @@ function OperationsComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="diff" className="mt-0">
|
||||
|
||||
@@ -6,13 +6,13 @@ import { useState } from 'react'
|
||||
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { ScheduleAgendaPanel } from '@/components/schedule/schedule-agenda-panel'
|
||||
import { ScheduleJobsCard } from '@/components/schedule/schedule-jobs-card'
|
||||
import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { SectionCards, type SectionCardItem } from '@/components/section-cards'
|
||||
import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
|
||||
import { SectionCardsSkeleton } from '@/components/skeletons'
|
||||
|
||||
import { operationsJobsQueryOptions } from '@/queries/operations'
|
||||
@@ -37,12 +37,12 @@ function ScheduleComponent() {
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
|
||||
const items: SectionCardItem[] = [
|
||||
const items: KpiStatItem[] = [
|
||||
{
|
||||
label: 'Всего задач',
|
||||
value: jobs.length,
|
||||
icon: <ListTodo className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant="primary-light" size="sm">
|
||||
в выборке
|
||||
</Badge>
|
||||
@@ -52,7 +52,7 @@ function ScheduleComponent() {
|
||||
label: 'В работе',
|
||||
value: running,
|
||||
icon: <Clock className="size-4" />,
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant={running > 0 ? 'info-light' : 'outline'} size="sm">
|
||||
очередь и выполнение
|
||||
</Badge>
|
||||
@@ -63,7 +63,7 @@ function ScheduleComponent() {
|
||||
value: failed,
|
||||
icon: <AlertTriangle className="size-4" />,
|
||||
variant: failed > 0 ? 'warning' : 'default',
|
||||
badge: (
|
||||
footer: (
|
||||
<Badge variant={failed > 0 ? 'warning-light' : 'success-light'} size="sm">
|
||||
{failed > 0 ? 'требуют внимания' : 'без ошибок'}
|
||||
</Badge>
|
||||
@@ -108,11 +108,11 @@ function ScheduleComponent() {
|
||||
}
|
||||
/>
|
||||
|
||||
{loading ? <SectionCardsSkeleton count={3} /> : <SectionCards items={items} />}
|
||||
{loading ? <SectionCardsSkeleton count={3} /> : <KpiStatGrid items={items} />}
|
||||
|
||||
<ScheduleAgendaPanel jobs={jobs} isLoading={jobsQ.isLoading} />
|
||||
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Модули"
|
||||
description="Расписание обновления и ручной запуск обновления"
|
||||
>
|
||||
@@ -134,7 +134,7 @@ function ScheduleComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
|
||||
<ScheduleJobsCard
|
||||
jobs={jobs}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PanelCard } from '@/components/panel-card'
|
||||
import { Input } from '@evobgp/ui/components/input'
|
||||
import { Label } from '@evobgp/ui/components/label'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { SelectField } from '@/components/select-field'
|
||||
import { SettingsKvGrid } from '@/components/settings/settings-kv-grid'
|
||||
import { SettingsSettingField } from '@/components/settings/settings-setting-field'
|
||||
@@ -335,7 +335,7 @@ function TenantSettingsComponent() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="additional" className="mt-0">
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="Дополнительные параметры"
|
||||
description="Параметры вне стандартных групп (только чтение — изменяются через API)"
|
||||
>
|
||||
@@ -356,7 +356,7 @@ function TenantSettingsComponent() {
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
</TabsContent>
|
||||
</BadgeTabs>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user