From c3369059afece85c8538c958013a63645ca6ab10 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 31 Jul 2026 12:15:59 +0700 Subject: [PATCH] feat(web): align ops screens with ReUI PRO kit and Frame surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpsDashboard+afterKpi на dashboard; FrameDataGrid вместо DataGridCard; KpiStatGrid вместо SectionCards; SettingsShell без Separator. Preview: dashboard-1, stats-12, data-grid-filtering-2, settings-16. Co-authored-by: Cursor --- .../access/access-api-keys-card.tsx | 6 +- .../dashboard/dashboard-kpi-grid.tsx | 19 +++ apps/web/src/components/data-grid-shell.tsx | 26 ++-- .../components/lookup/lookup-matches-grid.tsx | 7 +- .../modules/module-entries-section.tsx | 6 +- .../operations/operations-jobs-card.tsx | 6 +- .../components/reui-kit/frame-data-grid.tsx | 53 ++++++++ apps/web/src/components/reui-kit/index.ts | 1 + .../src/components/reui-kit/ops-dashboard.tsx | 14 ++- .../components/reui-kit/settings-shell.tsx | 11 +- .../schedule/schedule-jobs-card.tsx | 6 +- apps/web/src/components/section-cards.tsx | 61 --------- apps/web/src/routes/_auth/access.tsx | 12 +- apps/web/src/routes/_auth/dashboard.tsx | 119 +++++++++--------- apps/web/src/routes/_auth/directories.tsx | 25 ++-- apps/web/src/routes/_auth/modules/index.tsx | 6 +- apps/web/src/routes/_auth/monitoring.tsx | 6 +- apps/web/src/routes/_auth/operations.tsx | 6 +- apps/web/src/routes/_auth/schedule.tsx | 18 +-- apps/web/src/routes/_auth/tenant-settings.tsx | 6 +- 20 files changed, 216 insertions(+), 198 deletions(-) create mode 100644 apps/web/src/components/reui-kit/frame-data-grid.tsx delete mode 100644 apps/web/src/components/section-cards.tsx diff --git a/apps/web/src/components/access/access-api-keys-card.tsx b/apps/web/src/components/access/access-api-keys-card.tsx index ad69634..92d0c67 100644 --- a/apps/web/src/components/access/access-api-keys-card.tsx +++ b/apps/web/src/components/access/access-api-keys-card.tsx @@ -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 ( <> - )} - + ({ + ...card, + id: card.id ?? String(card.label), + })) +} + export function DashboardKpiGrid({ modules, peers, diff --git a/apps/web/src/components/data-grid-shell.tsx b/apps/web/src/components/data-grid-shell.tsx index 645d181..98cb142 100644 --- a/apps/web/src/components/data-grid-shell.tsx +++ b/apps/web/src/components/data-grid-shell.tsx @@ -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({ ) } -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 ( - - {children} - +
{children}
+ ) } diff --git a/apps/web/src/components/lookup/lookup-matches-grid.tsx b/apps/web/src/components/lookup/lookup-matches-grid.tsx index f4094e8..f9f370b 100644 --- a/apps/web/src/components/lookup/lookup-matches-grid.tsx +++ b/apps/web/src/components/lookup/lookup-matches-grid.tsx @@ -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 ( - @@ -128,6 +129,6 @@ export function LookupMatchesGrid({ void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } }) } /> - + ) } diff --git a/apps/web/src/components/modules/module-entries-section.tsx b/apps/web/src/components/modules/module-entries-section.tsx index c2dbc11..bbd942e 100644 --- a/apps/web/src/components/modules/module-entries-section.tsx +++ b/apps/web/src/components/modules/module-entries-section.tsx @@ -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 ( <> - )} - + {mod.type === 'DOMAINS' ? ( filterJobs(jobs, tab), [jobs, tab]) return ( - +
setTab(v as JobTab)} className="w-full"> @@ -81,6 +81,6 @@ export function OperationsJobsCard({ /> )} - + ) } diff --git a/apps/web/src/components/reui-kit/frame-data-grid.tsx b/apps/web/src/components/reui-kit/frame-data-grid.tsx new file mode 100644 index 0000000..1702615 --- /dev/null +++ b/apps/web/src/components/reui-kit/frame-data-grid.tsx @@ -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 ( + + + {hasHeader ? ( + +
+ {title ? {title} : null} + {description ? ( + {description} + ) : null} +
+ {actions ? ( +
+ {actions} +
+ ) : null} +
+ ) : null} +
{children}
+
+ + ) +} diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 7f24e89..30f65e6 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -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' diff --git a/apps/web/src/components/reui-kit/ops-dashboard.tsx b/apps/web/src/components/reui-kit/ops-dashboard.tsx index 6f66f41..089b617 100644 --- a/apps/web/src/components/reui-kit/ops-dashboard.tsx +++ b/apps/web/src/components/reui-kit/ops-dashboard.tsx @@ -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 (
- +
+ {afterKpi ?
{afterKpi}
: null} +
-
- - -
+
filterJobs(jobs, tab), [jobs, tab]) return ( - +
setTab(v as JobTab)} className="w-full"> @@ -72,6 +72,6 @@ export function ScheduleJobsCard({ /> )} - + ) } diff --git a/apps/web/src/components/section-cards.tsx b/apps/web/src/components/section-cards.tsx deleted file mode 100644 index c8a291f..0000000 --- a/apps/web/src/components/section-cards.tsx +++ /dev/null @@ -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, string> = { - default: 'text-muted-foreground', - warning: 'text-warning', - destructive: 'text-destructive', -} - -function hintToFooter(hint: ReactNode) { - if (typeof hint === 'string') { - return ( - - {hint} - - ) - } - 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 ( - - ) -} diff --git a/apps/web/src/routes/_auth/access.tsx b/apps/web/src/routes/_auth/access.tsx index 674d051..9b8cfd8 100644 --- a/apps/web/src/routes/_auth/access.tsx +++ b/apps/web/src/routes/_auth/access.tsx @@ -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: , - badge: ( + footer: ( в tenant @@ -51,7 +51,7 @@ function AccessComponent() { label: 'Активных', value: activeCount, icon: , - badge: ( + footer: ( не отозваны @@ -62,7 +62,7 @@ function AccessComponent() { value: revokedCount, icon: , variant: revokedCount > 0 ? 'warning' : 'default', - badge: ( + footer: ( 0 ? 'warning-light' : 'outline'} size="sm"> revoked @@ -156,7 +156,7 @@ function AccessComponent() { {keysQuery.isLoading ? ( ) : ( - + )} (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 ( -
- - - Обновить - - } - /> - - {initialLoading ? ( - - ) : ( + + + Обновить + + } + kpiCards={kpiCards} + isLoading={initialLoading} + afterKpi={showQuickActions ? : null} + charts={ <> - - - {showQuickActions ? : null} -
@@ -120,39 +112,40 @@ function DashboardComponent() { />
-
- )} - -
- - {activityLoading ? ( - - ) : ( - - )} - - - - {activityLoading ? ( - - ) : ( - - )} - -
-
+ } + queueTitle="Недавняя активность" + queueDescription="Задачи и ревизии плоскости управления" + queue={ +
+ + {activityLoading ? ( + + ) : ( + + )} + + + {activityLoading ? ( + + ) : ( + + )} + +
+ } + /> ) } diff --git a/apps/web/src/routes/_auth/directories.tsx b/apps/web/src/routes/_auth/directories.tsx index aa84716..1a8da1f 100644 --- a/apps/web/src/routes/_auth/directories.tsx +++ b/apps/web/src/routes/_auth/directories.tsx @@ -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: , - badge: ( + footer: ( теги префиксов @@ -41,7 +36,7 @@ function DirectoriesComponent() { label: 'DoH профили', value: dohProfiles.length, icon: , - badge: ( + footer: ( резолвинг доменов @@ -51,7 +46,7 @@ function DirectoriesComponent() { label: 'Справочники', value: 'Общие', icon: , - badge: ( + footer: ( все модули tenant @@ -80,7 +75,7 @@ function DirectoriesComponent() { } /> - {loading ? : } + {loading ? : } - @@ -111,11 +106,11 @@ function DirectoriesComponent() { /> )} - + - + )} - +
diff --git a/apps/web/src/routes/_auth/modules/index.tsx b/apps/web/src/routes/_auth/modules/index.tsx index 8bfe081..78c793e 100644 --- a/apps/web/src/routes/_auth/modules/index.tsx +++ b/apps/web/src/routes/_auth/modules/index.tsx @@ -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() { } /> - }> @@ -65,7 +65,7 @@ function ModulesListComponent() { /> )} - +
) } diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx index a86ecf4..591adb9 100644 --- a/apps/web/src/routes/_auth/monitoring.tsx +++ b/apps/web/src/routes/_auth/monitoring.tsx @@ -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() { )}
- @@ -139,7 +139,7 @@ function MonitoringComponent() { > {(ready) => } - + - + )} - + diff --git a/apps/web/src/routes/_auth/schedule.tsx b/apps/web/src/routes/_auth/schedule.tsx index 4f3a68c..96f1eae 100644 --- a/apps/web/src/routes/_auth/schedule.tsx +++ b/apps/web/src/routes/_auth/schedule.tsx @@ -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: , - badge: ( + footer: ( в выборке @@ -52,7 +52,7 @@ function ScheduleComponent() { label: 'В работе', value: running, icon: , - badge: ( + footer: ( 0 ? 'info-light' : 'outline'} size="sm"> очередь и выполнение @@ -63,7 +63,7 @@ function ScheduleComponent() { value: failed, icon: , variant: failed > 0 ? 'warning' : 'default', - badge: ( + footer: ( 0 ? 'warning-light' : 'success-light'} size="sm"> {failed > 0 ? 'требуют внимания' : 'без ошибок'} @@ -108,11 +108,11 @@ function ScheduleComponent() { } /> - {loading ? : } + {loading ? : } - @@ -134,7 +134,7 @@ function ScheduleComponent() { /> )} - + - @@ -356,7 +356,7 @@ function TenantSettingsComponent() { /> )} - +