From 940f8892f356a17940587f6c3096c34d8e629f6b Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 17:23:17 +0700 Subject: [PATCH] refactor: enhance UI components with PanelCard integration and improved layout consistency Updated multiple components to utilize the new PanelCard for better organization and presentation of content. Refactored DataGridShell, DataGridCard, and various analytics components to streamline layouts and enhance user experience. Adjusted styles for consistency across components, including pagination and toolbar elements, ensuring a cohesive interface throughout the application. --- .../analytics/analytics-card-shell.tsx | 66 ++++++------- .../analytics/analytics-progress.tsx | 7 +- .../dashboard-network-capacity-card.tsx | 6 +- .../analytics/dashboard-platform-card.tsx | 18 ++-- .../analytics/monitoring-health-card.tsx | 6 +- .../network-overview-analytics-card.tsx | 12 +-- .../dashboard/dashboard-network-panel.tsx | 6 +- .../dashboard/dashboard-quick-actions.tsx | 9 +- .../dashboard/dashboard-recent-jobs-grid.tsx | 5 +- apps/web/src/components/data-grid-shell.tsx | 31 +++--- apps/web/src/components/data-grid-toolbar.tsx | 2 +- .../firewall/firewall-clients-grid.tsx | 8 +- apps/web/src/components/layout/app-shell.tsx | 6 +- .../components/modules/modules-list-grid.tsx | 3 +- .../monitoring/monitoring-ready-grid.tsx | 13 +-- .../components/network/network-peers-grid.tsx | 12 ++- .../network/network-speakers-grid.tsx | 13 +-- .../network/speaker-form-dialog.tsx | 10 +- .../operations/operations-jobs-grid.tsx | 5 +- apps/web/src/components/panel-card.tsx | 64 ++++++++++++ .../reui/data-grid/data-grid-pagination.tsx | 8 +- .../data-grid/data-grid-table-virtual.tsx | 5 +- .../reui/data-grid/data-grid-table.tsx | 13 +-- .../schedule/schedule-jobs-grid.tsx | 5 +- .../schedule/schedule-modules-grid.tsx | 3 +- apps/web/src/components/section-cards.tsx | 5 +- apps/web/src/components/skeletons.tsx | 18 ++-- apps/web/src/components/status-badge.tsx | 3 +- apps/web/src/lib/access/api-key-labels.ts | 10 +- apps/web/src/lib/data-grid-defaults.ts | 11 +++ .../src/lib/metrics/deployment-progress.ts | 22 +++++ .../src/lib/metrics/peer-session-breakdown.ts | 4 +- .../src/lib/metrics/readiness-breakdown.ts | 8 +- .../lib/metrics/recent-platform-activity.ts | 14 +-- apps/web/src/lib/ui-labels.ts | 94 ++++++++++++++++++ apps/web/src/routes/_auth/access.tsx | 36 +++---- apps/web/src/routes/_auth/dashboard.tsx | 20 ++-- apps/web/src/routes/_auth/firewall.tsx | 38 ++++---- apps/web/src/routes/_auth/monitoring.tsx | 97 ++++++++++--------- apps/web/src/routes/_auth/network.tsx | 35 +++---- apps/web/src/routes/_auth/operations.tsx | 19 ++-- apps/web/src/routes/_auth/schedule.tsx | 4 +- apps/web/src/routes/_auth/settings.tsx | 35 +++---- apps/web/src/routes/_auth/tenant-settings.tsx | 58 ++++++----- apps/web/tsconfig.tsbuildinfo | 2 +- packages/ui/src/components/progress.tsx | 53 +++++----- 46 files changed, 552 insertions(+), 370 deletions(-) create mode 100644 apps/web/src/components/panel-card.tsx diff --git a/apps/web/src/components/analytics/analytics-card-shell.tsx b/apps/web/src/components/analytics/analytics-card-shell.tsx index 6b61b58..09f6679 100644 --- a/apps/web/src/components/analytics/analytics-card-shell.tsx +++ b/apps/web/src/components/analytics/analytics-card-shell.tsx @@ -1,14 +1,6 @@ import type { ReactNode } from 'react' import { Info } from 'lucide-react' -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@evobgp/ui/components/card' import { cn } from '@evobgp/ui/lib/utils' import { Tooltip, @@ -16,6 +8,8 @@ import { TooltipTrigger, } from '@evobgp/ui/components/tooltip' +import { PanelCard } from '@/components/panel-card' + export function AnalyticsCardShell({ title, description, @@ -33,32 +27,36 @@ export function AnalyticsCardShell({ className?: string children: ReactNode }) { + const titleNode = ( + + {title} + {info ? ( + + + + + + {info} + + + ) : null} + + ) + return ( - - -
- - {title} - {info ? ( - - - - - - {info} - - - ) : null} - - {description ? {description} : null} -
- {actions ?
{actions}
: null} -
- {children} - {footer ? {footer} : null} -
+ + {children} + ) } diff --git a/apps/web/src/components/analytics/analytics-progress.tsx b/apps/web/src/components/analytics/analytics-progress.tsx index d9420cd..8d2768b 100644 --- a/apps/web/src/components/analytics/analytics-progress.tsx +++ b/apps/web/src/components/analytics/analytics-progress.tsx @@ -7,10 +7,12 @@ import { cn } from '@evobgp/ui/lib/utils' export function AnalyticsProgress({ label, + hint, value, className, }: { label: string + hint?: string value: number className?: string }) { @@ -21,9 +23,10 @@ export function AnalyticsProgress({ {label} {clamped}% - + {hint ?

{hint}

: null} + - + diff --git a/apps/web/src/components/analytics/dashboard-network-capacity-card.tsx b/apps/web/src/components/analytics/dashboard-network-capacity-card.tsx index f0bf5ff..54dac40 100644 --- a/apps/web/src/components/analytics/dashboard-network-capacity-card.tsx +++ b/apps/web/src/components/analytics/dashboard-network-capacity-card.tsx @@ -50,14 +50,14 @@ export function DashboardNetworkCapacityCard({ const deltaLabel = mode === 'peers' - ? `${peers.filter((p) => p.enabled !== false && p.session_state === 'Established').length} Established` - : `${speakers.filter((s) => s.live?.agent_ok).length} online` + ? `${peers.filter((p) => p.enabled !== false && p.session_state === 'Established').length} установлено` + : `${speakers.filter((s) => s.live?.agent_ok).length} в сети` return ( 0 ? Math.round((peersEstablished / peersEnabled) * 100) : null const deploy = useMemo(() => deploymentProgress(speakers), [speakers]) + const deployMeta = useMemo(() => deploymentProgressMeta(deploy), [deploy]) const activity = useMemo( () => recentPlatformActivity(jobs, revisions, peers, speakers), [jobs, revisions, peers, speakers], @@ -74,7 +76,7 @@ export function DashboardPlatformCard({ label: bgpPct === null ? 'нет включённых пиров' - : `${peersEstablished} Established`, + : `${peersEstablished} установлено`, tone: (bgpPct !== null && bgpPct >= 90 ? 'success' : bgpPct !== null && bgpPct < 70 @@ -87,16 +89,14 @@ export function DashboardPlatformCard({ value: loading ? '—' : String(riskCount), delta: { direction: (riskCount > 0 ? 'down' : 'up') as 'up' | 'down', - label: riskCount > 0 ? `${failedJobs} задач, ${peersMismatch} mismatch` : 'в норме', + label: riskCount > 0 ? `${failedJobs} задач, ${peersMismatch} расхождений` : 'в норме', tone: (riskCount > 0 ? 'destructive' : 'success') as 'destructive' | 'success', }, }, ] - const progressLabel = - deploy.mode === 'revision' - ? `Синхронизация ревизий (${deploy.synced}/${deploy.total})` - : `Спикеры online (${deploy.synced}/${deploy.total})` + const progressLabel = deployMeta.label + const progressHint = deployMeta.hint return ( - +
Недавняя активность diff --git a/apps/web/src/components/analytics/monitoring-health-card.tsx b/apps/web/src/components/analytics/monitoring-health-card.tsx index d14d6e4..aac7c89 100644 --- a/apps/web/src/components/analytics/monitoring-health-card.tsx +++ b/apps/web/src/components/analytics/monitoring-health-card.tsx @@ -18,15 +18,15 @@ export function MonitoringHealthCard({ return ( {loading ? (
Загрузка…
) : ( - + )}
) diff --git a/apps/web/src/components/analytics/network-overview-analytics-card.tsx b/apps/web/src/components/analytics/network-overview-analytics-card.tsx index 57acd50..51968e5 100644 --- a/apps/web/src/components/analytics/network-overview-analytics-card.tsx +++ b/apps/web/src/components/analytics/network-overview-analytics-card.tsx @@ -21,29 +21,29 @@ export function NetworkOverviewAnalyticsCard({ return ( 0 ? 'down' : 'up', - label: net.peersMismatch > 0 ? `${net.peersMismatch} mismatch` : 'сессии в норме', + label: net.peersMismatch > 0 ? `${net.peersMismatch} расхождений` : 'сессии в норме', tone: net.peersMismatch > 0 ? 'warning' : 'success', }, }, { - label: 'Спикеры online', + label: 'Спикеры в сети', value: loading ? '—' : `${net.speakersOnline}/${net.speakersTotal}`, delta: { direction: net.speakersOnline < net.speakersTotal ? 'down' : 'up', label: net.speakersOnline < net.speakersTotal - ? `${net.speakersTotal - net.speakersOnline} offline` - : 'все online', + ? `${net.speakersTotal - net.speakersOnline} не в сети` + : 'все в сети', tone: net.speakersOnline < net.speakersTotal ? 'warning' : 'success', }, }, diff --git a/apps/web/src/components/dashboard/dashboard-network-panel.tsx b/apps/web/src/components/dashboard/dashboard-network-panel.tsx index 1b34666..47bc8bb 100644 --- a/apps/web/src/components/dashboard/dashboard-network-panel.tsx +++ b/apps/web/src/components/dashboard/dashboard-network-panel.tsx @@ -28,10 +28,10 @@ export function DashboardNetworkPanel({ const m = aggregateNetworkMetrics(peers, speakers) return (
- - + + {m.peersMismatch > 0 ? ( - + ) : null}
) diff --git a/apps/web/src/components/dashboard/dashboard-quick-actions.tsx b/apps/web/src/components/dashboard/dashboard-quick-actions.tsx index 2338864..98b1411 100644 --- a/apps/web/src/components/dashboard/dashboard-quick-actions.tsx +++ b/apps/web/src/components/dashboard/dashboard-quick-actions.tsx @@ -2,18 +2,17 @@ import { Link } from '@tanstack/react-router' import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react' import { Button } from '@evobgp/ui/components/button' -import { CardFooter } from '@evobgp/ui/components/card' export function DashboardQuickActions() { return ( - +
- +
) } diff --git a/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx b/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx index 12be1c4..fd25a31 100644 --- a/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx @@ -6,6 +6,7 @@ import { DataGridSection } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DATA_GRID_DENSE_LAYOUT } from '@/lib/data-grid-defaults' +import { jobKindRu } from '@/lib/ui-labels' import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { JobRow } from '@/types/api' @@ -27,7 +28,7 @@ export function DashboardRecentJobsGrid({ header: ({ column }) => , cell: ({ row }) => ( row.job_id, pageSize: 8, diff --git a/apps/web/src/components/data-grid-shell.tsx b/apps/web/src/components/data-grid-shell.tsx index 835efa6..3dd9435 100644 --- a/apps/web/src/components/data-grid-shell.tsx +++ b/apps/web/src/components/data-grid-shell.tsx @@ -1,13 +1,15 @@ import type { ReactNode } from 'react' import type { Table } from '@tanstack/react-table' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { cn } from '@evobgp/ui/lib/utils' import { DataGridToolbar } from '@/components/data-grid-toolbar' +import { PanelCard, panelCardContentFlushClassName, panelCardFooterClassName } from '@/components/panel-card' 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' import { + DATA_GRID_MESSAGES_RU, DATA_GRID_PAGINATION_RU, DATA_GRID_TABLE_LAYOUT, } from '@/lib/data-grid-defaults' @@ -38,7 +40,8 @@ export function DataGridShell({ table={table} recordCount={recordCount} isLoading={isLoading} - emptyMessage={emptyMessage} + emptyMessage={emptyMessage ?? DATA_GRID_MESSAGES_RU.emptyMessage} + loadingMessage={DATA_GRID_MESSAGES_RU.loadingMessage} tableLayout={tableLayout} className={className} onRowClick={onRowClick} @@ -47,7 +50,7 @@ export function DataGridShell({ {showPagination ? ( -
+
) : null} @@ -64,20 +67,16 @@ interface DataGridCardProps { } export function DataGridCard({ title, description, actions, children, className }: DataGridCardProps) { - const hasHeader = Boolean(title || description || actions) return ( - - {hasHeader ? ( - -
- {title ? {title} : null} - {description ? {description} : null} -
- {actions ?
{actions}
: null} -
- ) : null} - {children} -
+ + {children} + ) } diff --git a/apps/web/src/components/data-grid-toolbar.tsx b/apps/web/src/components/data-grid-toolbar.tsx index 2c4cab6..c429123 100644 --- a/apps/web/src/components/data-grid-toolbar.tsx +++ b/apps/web/src/components/data-grid-toolbar.tsx @@ -28,7 +28,7 @@ export function DataGridToolbar({ className, }: DataGridToolbarProps) { return ( -
+
diff --git a/apps/web/src/components/firewall/firewall-clients-grid.tsx b/apps/web/src/components/firewall/firewall-clients-grid.tsx index 8168719..345d869 100644 --- a/apps/web/src/components/firewall/firewall-clients-grid.tsx +++ b/apps/web/src/components/firewall/firewall-clients-grid.tsx @@ -60,7 +60,7 @@ export function FirewallClientsGrid({ { id: 'last_seen_at', accessorFn: (row) => row.last_seen_at ?? '', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( {row.original.last_seen_at?.slice(0, 19) ?? '—'} ), @@ -69,12 +69,12 @@ export function FirewallClientsGrid({ const bv = b.original.last_seen_at ?? '' return av.localeCompare(bv) }, - meta: { headerTitle: 'Last seen' }, + meta: { headerTitle: 'Последняя активность' }, }, { id: 'apply', enableSorting: false, - header: 'Apply', + header: 'Применение', cell: ({ row }) => { const c = row.original return ( @@ -84,7 +84,7 @@ export function FirewallClientsGrid({ ) }, - meta: { headerTitle: 'Apply' }, + meta: { headerTitle: 'Применение' }, }, { id: 'packets', diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index aa9d5bd..85169e9 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -56,7 +56,7 @@ interface NavGroup { const NAV_GROUPS: NavGroup[] = [ { label: 'Обзор', - items: [{ to: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }], + items: [{ to: '/dashboard', label: 'Панель', icon: LayoutDashboard }], }, { label: 'Маршрутизация', @@ -70,7 +70,7 @@ const NAV_GROUPS: NavGroup[] = [ label: 'Операции', items: [ { to: '/operations', label: 'Операции', icon: Cog }, - { to: '/firewall', label: 'Firewall', icon: Shield }, + { to: '/firewall', label: 'Файрвол', icon: Shield }, { to: '/schedule', label: 'Задачи', icon: ListChecks }, { to: '/monitoring', label: 'Мониторинг', icon: Activity }, ], @@ -111,7 +111,7 @@ export function AppShell({ children }: { children: ReactNode }) {
EvoBGP - Control Plane + Плоскость управления
diff --git a/apps/web/src/components/modules/modules-list-grid.tsx b/apps/web/src/components/modules/modules-list-grid.tsx index b223275..69aa2ad 100644 --- a/apps/web/src/components/modules/modules-list-grid.tsx +++ b/apps/web/src/components/modules/modules-list-grid.tsx @@ -8,6 +8,7 @@ import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-c import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { useClientDataGrid } from '@/hooks/use-client-data-grid' +import { moduleTypeRu } from '@/lib/ui-labels' import type { ModuleRow } from '@/types/api' export function ModulesListGrid({ @@ -35,7 +36,7 @@ export function ModulesListGrid({ { accessorKey: 'type', header: ({ column }) => , - cell: ({ row }) => {row.original.type}, + cell: ({ row }) => {moduleTypeRu(row.original.type)}, meta: { headerTitle: 'Тип' }, }, { diff --git a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx index fbe7375..76091d1 100644 --- a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx +++ b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx @@ -7,6 +7,7 @@ import { DataGridSection } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { useClientDataGrid } from '@/hooks/use-client-data-grid' +import { jobStatusRu, readyCheckRu } from '@/lib/ui-labels' import type { ReadyStatus } from '@/queries/monitoring' const READY_CHECK_ICONS: Record = { @@ -36,19 +37,19 @@ export function MonitoringReadyGrid({ const rows: ReadyCheckRow[] = [ { id: 'liveness', - label: 'Liveness', + label: 'Живучесть', subtitle: '/v1/health', icon: HeartPulse, status: health?.ok ? 'ok' : 'error', - statusLabel: health?.ok ? 'OK' : 'Ошибка', + statusLabel: health?.ok ? 'В норме' : 'Ошибка', }, { id: 'readiness', - label: 'Readiness', + label: 'Готовность', subtitle: '/v1/ready', icon: ShieldCheck, status: ready.status === 'ok' ? 'ok' : 'warning', - statusLabel: ready.status ?? '—', + statusLabel: ready.status === 'ok' ? 'Готов' : jobStatusRu(ready.status ?? 'pending'), }, ] for (const key of Object.keys(checks)) { @@ -56,10 +57,10 @@ export function MonitoringReadyGrid({ const ok = typeof value === 'boolean' ? value : value?.ok !== false rows.push({ id: key, - label: key, + label: readyCheckRu(key), icon: READY_CHECK_ICONS[key] ?? ListTodo, status: ok ? 'ok' : 'error', - statusLabel: ok ? 'OK' : 'Ошибка', + statusLabel: ok ? 'В норме' : 'Ошибка', }) } return rows diff --git a/apps/web/src/components/network/network-peers-grid.tsx b/apps/web/src/components/network/network-peers-grid.tsx index af941ca..7d0b28a 100644 --- a/apps/web/src/components/network/network-peers-grid.tsx +++ b/apps/web/src/components/network/network-peers-grid.tsx @@ -7,6 +7,7 @@ import { DataGridSection } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { useClientDataGrid } from '@/hooks/use-client-data-grid' +import { bgpSessionStateRu } from '@/lib/ui-labels' import type { PeerRow } from '@/types/api' export function NetworkPeersGrid({ @@ -33,11 +34,11 @@ export function NetworkPeersGrid({ }, { accessorKey: 'neighbor', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( ), - meta: { headerTitle: 'Neighbor' }, + meta: { headerTitle: 'Адрес соседа' }, }, { accessorKey: 'remote_asn', @@ -52,9 +53,12 @@ export function NetworkPeersGrid({ header: ({ column }) => , cell: ({ row }) => (
- + {row.original.session_mismatch ? ( - mismatch + расхождение ) : null}
), diff --git a/apps/web/src/components/network/network-speakers-grid.tsx b/apps/web/src/components/network/network-speakers-grid.tsx index 2797e8c..f17cc9e 100644 --- a/apps/web/src/components/network/network-speakers-grid.tsx +++ b/apps/web/src/components/network/network-speakers-grid.tsx @@ -8,6 +8,7 @@ import { StatusBadge } from '@/components/status-badge' 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' +import { speakerOnlineLabel } from '@/lib/ui-labels' import type { SpeakerRow } from '@/types/api' export function NetworkSpeakersGrid({ @@ -21,11 +22,11 @@ export function NetworkSpeakersGrid({ () => [ { accessorKey: 'endpoint', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => ( ), - meta: { headerTitle: 'Endpoint' }, + meta: { headerTitle: 'Конечная точка' }, }, { accessorKey: 'role', @@ -36,14 +37,14 @@ export function NetworkSpeakersGrid({ { id: 'agent', enableSorting: false, - header: 'Agent', + header: 'Агент', cell: ({ row }) => { const live = row.original.live - if (live?.agent_ok === true) return - if (live?.agent_ok === false) return + if (live?.agent_ok === true) return + if (live?.agent_ok === false) return return }, - meta: { headerTitle: 'Agent' }, + meta: { headerTitle: 'Агент' }, }, { id: 'bgp', diff --git a/apps/web/src/components/network/speaker-form-dialog.tsx b/apps/web/src/components/network/speaker-form-dialog.tsx index 56010ac..19ad0ea 100644 --- a/apps/web/src/components/network/speaker-form-dialog.tsx +++ b/apps/web/src/components/network/speaker-form-dialog.tsx @@ -109,7 +109,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps } >
- + setRole(v ?? 'replica')} />
- +
- + , cell: ({ row }) => ( row.job_id, }) diff --git a/apps/web/src/components/panel-card.tsx b/apps/web/src/components/panel-card.tsx new file mode 100644 index 0000000..bdc8907 --- /dev/null +++ b/apps/web/src/components/panel-card.tsx @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react' + +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@evobgp/ui/components/card' +import { cn } from '@evobgp/ui/lib/utils' + +/** REUI c-data-grid-19: flush card shell with compact header spacing. */ +export const panelCardClassName = 'gap-0 py-0' +export const panelCardHeaderClassName = 'border-b [.border-b]:pb-3' +export const panelCardContentFlushClassName = 'px-0' +export const panelCardFooterClassName = 'border-t bg-transparent py-0' + +interface PanelCardProps { + title?: ReactNode + description?: ReactNode + actions?: ReactNode + footer?: ReactNode + children?: ReactNode + className?: string + headerClassName?: string + contentClassName?: string + footerClassName?: string + size?: 'default' | 'sm' +} + +export function PanelCard({ + title, + description, + actions, + footer, + children, + className, + headerClassName, + contentClassName, + footerClassName, + size = 'sm', +}: PanelCardProps) { + const hasHeader = Boolean(title || description || actions) + + return ( + + {hasHeader ? ( + + {title ? {title} : null} + {description ? {description} : null} + {actions ? {actions} : null} + + ) : null} + {children != null && children !== false ? ( + {children} + ) : null} + {footer ? ( + {footer} + ) : null} + + ) +} diff --git a/apps/web/src/components/reui/data-grid/data-grid-pagination.tsx b/apps/web/src/components/reui/data-grid/data-grid-pagination.tsx index eeca2c4..1622695 100644 --- a/apps/web/src/components/reui/data-grid/data-grid-pagination.tsx +++ b/apps/web/src/components/reui/data-grid/data-grid-pagination.tsx @@ -140,12 +140,12 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element { mergedProps?.className )} > -
+
{isLoading ? ( mergedProps?.sizesSkeleton ) : ( <> -
+
{mergedProps.rowsPerPageLabel}
{ if (!value) return table.setPageSize(Number(value)) diff --git a/apps/web/src/components/reui/data-grid/data-grid-table-virtual.tsx b/apps/web/src/components/reui/data-grid/data-grid-table-virtual.tsx index b89fcad..6a40145 100644 --- a/apps/web/src/components/reui/data-grid/data-grid-table-virtual.tsx +++ b/apps/web/src/components/reui/data-grid/data-grid-table-virtual.tsx @@ -21,6 +21,7 @@ import { DataGridTableViewport, getDataGridTableRowSections, } from "@/components/reui/data-grid/data-grid-table" +import { DATA_GRID_MESSAGES_RU } from "@/lib/data-grid-defaults" import { flexRender, HeaderGroup, Row, Table } from "@tanstack/react-table" import { useVirtualizer, @@ -304,9 +305,9 @@ function DataGridTableVirtual({ const isVirtualizationEnabled = virtualizerOptions?.enabled !== false const loadingMoreMessage = - props.fetchingMoreMessage || props.loadingMessage || "Loading..." + props.fetchingMoreMessage || props.loadingMessage || DATA_GRID_MESSAGES_RU.loadingMessage const allRowsLoadedMessage = - props.allRowsLoadedMessage || "All records loaded" + props.allRowsLoadedMessage || DATA_GRID_MESSAGES_RU.allRecordsLoadedMessage const handleViewportRef = useCallback((node: HTMLDivElement | null) => { setViewportElements({ diff --git a/apps/web/src/components/reui/data-grid/data-grid-table.tsx b/apps/web/src/components/reui/data-grid/data-grid-table.tsx index 3277028..33ee2bd 100644 --- a/apps/web/src/components/reui/data-grid/data-grid-table.tsx +++ b/apps/web/src/components/reui/data-grid/data-grid-table.tsx @@ -28,6 +28,7 @@ import { cva } from "class-variance-authority" import { cn } from "@evobgp/ui/lib/utils" import { Checkbox } from "@evobgp/ui/components/checkbox" import { Spinner } from "@evobgp/ui/components/spinner" +import { DATA_GRID_MESSAGES_RU } from "@/lib/data-grid-defaults" const headerCellSpacingVariants = cva("", { variants: { @@ -1098,7 +1099,7 @@ function DataGridTableEmpty() { colSpan={Math.max(visibleColumnCount, 1)} className="text-muted-foreground text-sm py-6 text-center" > - {props.emptyMessage || "No data available"} + {props.emptyMessage || DATA_GRID_MESSAGES_RU.emptyMessage} ) @@ -1111,7 +1112,7 @@ function DataGridTableLoader() {
- {props.loadingMessage || "Loading..."} + {props.loadingMessage || DATA_GRID_MESSAGES_RU.loadingMessage}
) @@ -1123,7 +1124,7 @@ function DataGridTableRowPin({ row }: { row: Row }) { return (
diff --git a/apps/web/src/components/schedule/schedule-jobs-grid.tsx b/apps/web/src/components/schedule/schedule-jobs-grid.tsx index 3fa0e2f..e90072f 100644 --- a/apps/web/src/components/schedule/schedule-jobs-grid.tsx +++ b/apps/web/src/components/schedule/schedule-jobs-grid.tsx @@ -6,6 +6,7 @@ import { DataGridSection } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { useClientDataGrid } from '@/hooks/use-client-data-grid' +import { jobKindRu } from '@/lib/ui-labels' import type { JobRow } from '@/types/api' export function ScheduleJobsGrid({ @@ -20,7 +21,7 @@ export function ScheduleJobsGrid({ { accessorKey: 'kind', header: ({ column }) => , - cell: ({ row }) => , + cell: ({ row }) => , meta: { headerTitle: 'Вид' }, }, { @@ -79,7 +80,7 @@ export function ScheduleJobsGrid({ const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - getSearchText: (row) => `${row.kind} ${row.status} ${row.error ?? ''}`, + getSearchText: (row) => `${jobKindRu(row.kind)} ${row.status} ${row.error ?? ''}`, getRowId: (row) => row.job_id, }) diff --git a/apps/web/src/components/schedule/schedule-modules-grid.tsx b/apps/web/src/components/schedule/schedule-modules-grid.tsx index cb81c2c..dcfc504 100644 --- a/apps/web/src/components/schedule/schedule-modules-grid.tsx +++ b/apps/web/src/components/schedule/schedule-modules-grid.tsx @@ -8,6 +8,7 @@ import { DataGridSection } from '@/components/data-grid-shell' import { LoadingButton } from '@/components/loading-button' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { useClientDataGrid } from '@/hooks/use-client-data-grid' +import { moduleTypeRu } from '@/lib/ui-labels' import type { ModuleRow } from '@/types/api' export function ScheduleModulesGrid({ @@ -32,7 +33,7 @@ export function ScheduleModulesGrid({ { accessorKey: 'type', header: ({ column }) => , - cell: ({ row }) => {row.original.type}, + cell: ({ row }) => {moduleTypeRu(row.original.type)}, meta: { headerTitle: 'Тип' }, }, { diff --git a/apps/web/src/components/section-cards.tsx b/apps/web/src/components/section-cards.tsx index 04c55f6..2e17244 100644 --- a/apps/web/src/components/section-cards.tsx +++ b/apps/web/src/components/section-cards.tsx @@ -42,7 +42,7 @@ export function SectionCards({ items, className }: { items: SectionCardItem[]; c {items.map((item, idx) => { const clickable = Boolean(item.onClick) const content = ( - + {item.icon ? ( {item.icon} @@ -80,8 +80,9 @@ export function SectionCards({ items, className }: { items: SectionCardItem[]; c return ( - - + +
{Array.from({ length: 3 }).map((_, i) => ( @@ -30,15 +30,15 @@ export function AnalyticsDashboardSkeleton() { - - + + - - + + @@ -49,16 +49,16 @@ export function AnalyticsDashboardSkeleton() { export function TableSkeleton({ rows = 6, cols = 4 }: { rows?: number; cols?: number }) { return ( - +
-
+
{Array.from({ length: cols }).map((_, i) => ( ))}
{Array.from({ length: rows }).map((_, r) => ( -
+
{Array.from({ length: cols }).map((_, c) => ( ))} diff --git a/apps/web/src/components/status-badge.tsx b/apps/web/src/components/status-badge.tsx index 78cdee9..6913650 100644 --- a/apps/web/src/components/status-badge.tsx +++ b/apps/web/src/components/status-badge.tsx @@ -3,6 +3,7 @@ import type { ComponentProps } from 'react' import { cn } from '@evobgp/ui/lib/utils' import { Badge } from '@/components/reui/badge' +import { jobStatusRu } from '@/lib/ui-labels' type BadgeVariant = NonNullable['variant']> @@ -63,7 +64,7 @@ export function StatusBadge({
- {label ?? status} + {label ?? jobStatusRu(status)} {hint ? {hint} : null}
diff --git a/apps/web/src/lib/access/api-key-labels.ts b/apps/web/src/lib/access/api-key-labels.ts index 60dcd04..c51ed28 100644 --- a/apps/web/src/lib/access/api-key-labels.ts +++ b/apps/web/src/lib/access/api-key-labels.ts @@ -1,10 +1,12 @@ import type { ApiKeyRole } from '@/types/api' +import { apiKeyRoleRu } from '@/lib/ui-labels' + export const API_KEY_ROLE_ITEMS: ReadonlyArray<{ value: ApiKeyRole; label: string }> = [ - { value: 'viewer', label: 'viewer — только чтение' }, - { value: 'editor', label: 'editor — CRUD без apply' }, - { value: 'operator', label: 'operator — полный доступ' }, - { value: 'node', label: 'node — только API ноды' }, + { value: 'viewer', label: `${apiKeyRoleRu('viewer')} — только чтение` }, + { value: 'editor', label: `${apiKeyRoleRu('editor')} — CRUD без применения` }, + { value: 'operator', label: `${apiKeyRoleRu('operator')} — полный доступ` }, + { value: 'node', label: `${apiKeyRoleRu('node')} — только API ноды` }, ] export function apiKeyRoleLabel(role: ApiKeyRole): string { diff --git a/apps/web/src/lib/data-grid-defaults.ts b/apps/web/src/lib/data-grid-defaults.ts index d4d1053..70c4c70 100644 --- a/apps/web/src/lib/data-grid-defaults.ts +++ b/apps/web/src/lib/data-grid-defaults.ts @@ -25,6 +25,17 @@ export const DATA_GRID_PAGINATION_RU = { nextPageLabel: 'Следующая страница', } +export const DATA_GRID_MESSAGES_RU = { + emptyMessage: 'Нет данных', + loadingMessage: 'Загрузка…', + fetchingMoreMessage: 'Загрузка…', + selectAllLabel: 'Выбрать все', + selectRowLabel: 'Выбрать строку', + pinRowLabel: 'Закрепить строку', + unpinRowLabel: 'Открепить строку', + allRecordsLoadedMessage: 'Все записи загружены', +} + export const DATA_GRID_DENSE_LAYOUT: NonNullable['tableLayout']> = { ...DATA_GRID_TABLE_LAYOUT, dense: true, diff --git a/apps/web/src/lib/metrics/deployment-progress.ts b/apps/web/src/lib/metrics/deployment-progress.ts index 1df26bf..0a403d8 100644 --- a/apps/web/src/lib/metrics/deployment-progress.ts +++ b/apps/web/src/lib/metrics/deployment-progress.ts @@ -30,3 +30,25 @@ export function deploymentProgress(speakers: SpeakerRow[]): DeploymentProgress { mode: 'online', } } + +export function deploymentProgressMeta(deploy: DeploymentProgress): { + label: string + hint: string +} { + if (deploy.total === 0) { + return { + label: 'Деплой на спикерах', + hint: 'Нет зарегистрированных BIRD-спикеров', + } + } + if (deploy.mode === 'revision') { + return { + label: `Применена ревизия (${deploy.synced} из ${deploy.total} спикеров)`, + hint: 'Доля спикеров, на которых последняя опубликованная ревизия уже применена', + } + } + return { + label: `Спикеры в сети (${deploy.synced} из ${deploy.total})`, + hint: 'Ревизии ещё не публиковались — показана доступность агента на нодах', + } +} diff --git a/apps/web/src/lib/metrics/peer-session-breakdown.ts b/apps/web/src/lib/metrics/peer-session-breakdown.ts index c093c7f..86f9c14 100644 --- a/apps/web/src/lib/metrics/peer-session-breakdown.ts +++ b/apps/web/src/lib/metrics/peer-session-breakdown.ts @@ -14,7 +14,7 @@ export function peerSessionBreakdown(peers: PeerRow[]): BreakdownSlice[] { if (established > 0) { slices.push({ key: 'established', - label: 'Established', + label: 'Установлена', count: established, color: 'var(--color-chart-2)', }) @@ -22,7 +22,7 @@ export function peerSessionBreakdown(peers: PeerRow[]): BreakdownSlice[] { if (pending > 0) { slices.push({ key: 'pending', - label: 'Не Established', + label: 'Не установлена', count: pending, color: 'var(--color-warning)', }) diff --git a/apps/web/src/lib/metrics/readiness-breakdown.ts b/apps/web/src/lib/metrics/readiness-breakdown.ts index fc50d47..ddf0fad 100644 --- a/apps/web/src/lib/metrics/readiness-breakdown.ts +++ b/apps/web/src/lib/metrics/readiness-breakdown.ts @@ -35,7 +35,7 @@ export function readinessBreakdown( const slices: BreakdownSlice[] = [ { key: 'health', - label: 'Health OK', + label: 'API доступен', count: 1, color: 'var(--color-chart-2)', }, @@ -44,7 +44,7 @@ export function readinessBreakdown( if (okCount > 0) { slices.push({ key: 'checks-ok', - label: 'Checks OK', + label: 'Проверки в норме', count: okCount, color: 'var(--color-chart-1)', }) @@ -52,7 +52,7 @@ export function readinessBreakdown( if (failCount > 0) { slices.push({ key: 'checks-fail', - label: 'Checks fail', + label: 'Ошибки проверок', count: failCount, color: 'var(--color-warning)', }) @@ -61,7 +61,7 @@ export function readinessBreakdown( if (slices.length === 1 && okCount === 0 && failCount === 0) { slices.push({ key: 'ready', - label: ready?.status === 'ok' ? 'Ready' : 'Ready pending', + label: ready?.status === 'ok' ? 'Готов' : 'Ожидает готовности', count: 1, color: 'var(--color-chart-4)', }) diff --git a/apps/web/src/lib/metrics/recent-platform-activity.ts b/apps/web/src/lib/metrics/recent-platform-activity.ts index 275c2e2..2c6a82f 100644 --- a/apps/web/src/lib/metrics/recent-platform-activity.ts +++ b/apps/web/src/lib/metrics/recent-platform-activity.ts @@ -1,17 +1,11 @@ import type { JobRow, PeerRow, RevisionRow, SpeakerRow } from '@/types/api' +import { jobKindRu, jobStatusRu } from '@/lib/ui-labels' + import type { PlatformActivityItem } from './types' -const JOB_KIND_RU: Record = { - module_refresh: 'Обновление модуля', - apply: 'Применение конфигурации', - rollback: 'Откат ревизии', - bird_reload: 'Перезагрузка BIRD', -} - function jobMessage(job: JobRow): string { - const kind = JOB_KIND_RU[job.kind] ?? job.kind - return `${kind} · ${job.status}` + return `${jobKindRu(job.kind)} · ${jobStatusRu(job.status)}` } export function recentPlatformActivity( @@ -45,7 +39,7 @@ export function recentPlatformActivity( for (const peer of peers.filter((p) => p.session_mismatch).slice(0, 2)) { items.push({ id: `peer-${peer.id}`, - message: `Mismatch сессии: ${peer.name ?? peer.neighbor}`, + message: `Расхождение сессии: ${peer.name ?? peer.neighbor}`, status: 'mismatch', kind: 'network', }) diff --git a/apps/web/src/lib/ui-labels.ts b/apps/web/src/lib/ui-labels.ts index 35c1f8c..b03759e 100644 --- a/apps/web/src/lib/ui-labels.ts +++ b/apps/web/src/lib/ui-labels.ts @@ -27,3 +27,97 @@ export function moduleTypeRu(type: string): string { return type } } + +const JOB_KIND_RU: Record = { + module_refresh: 'Обновление модуля', + apply: 'Применение конфигурации', + rollback: 'Откат ревизии', + bird_reload: 'Перезагрузка BIRD', +} + +export function jobKindRu(kind: string): string { + return JOB_KIND_RU[kind] ?? kind +} + +const JOB_STATUS_RU: Record = { + queued: 'В очереди', + running: 'Выполняется', + succeeded: 'Успешно', + failed: 'Ошибка', + error: 'Ошибка', + cancelled: 'Отменена', + canceled: 'Отменена', + pending: 'Ожидает', + approved: 'Одобрен', + revoked: 'Отозван', + active: 'Активен', + ok: 'В норме', + mismatch: 'Расхождение', + established: 'Установлена', + healthy: 'В норме', + warning: 'Предупреждение', + stale: 'Устарело', + overdue: 'Просрочено', + paused: 'Приостановлен', + disabled: 'Выключен', + archived: 'В архиве', + block: 'block', + accept: 'accept', +} + +export function jobStatusRu(status: string): string { + return JOB_STATUS_RU[status.toLowerCase()] ?? status +} + +export function bgpSessionStateRu(state: string | null | undefined): string { + if (!state) return '—' + if (state === 'Established') return 'Установлена' + return state +} + +export function speakerOnlineLabel(agentOk: boolean | undefined): string { + if (agentOk === true) return 'В сети' + if (agentOk === false) return 'Не в сети' + return '—' +} + +export function firewallClientStatusRu(status: string): string { + switch (status) { + case 'pending': + return 'Ожидает' + case 'approved': + return 'Одобрен' + case 'revoked': + return 'Отозван' + default: + return status + } +} + +export function apiKeyRoleRu(role: string): string { + switch (role) { + case 'viewer': + return 'Наблюдатель' + case 'editor': + return 'Редактор' + case 'operator': + return 'Оператор' + case 'node': + return 'Нода' + default: + return role + } +} + +export function readyCheckRu(key: string): string { + switch (key) { + case 'postgres': + return 'PostgreSQL' + case 'store': + return 'Хранилище' + case 'jobs': + return 'Очередь задач' + default: + return key + } +} diff --git a/apps/web/src/routes/_auth/access.tsx b/apps/web/src/routes/_auth/access.tsx index 95f14ec..03394aa 100644 --- a/apps/web/src/routes/_auth/access.tsx +++ b/apps/web/src/routes/_auth/access.tsx @@ -4,7 +4,7 @@ import { KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { PanelCard } from '@/components/panel-card' import { AccessApiKeysCard } from '@/components/access/access-api-keys-card' import { PageHeader } from '@/components/page-header' @@ -79,12 +79,11 @@ function AccessComponent() { /> {session ? ( - - - Текущая сессия - Tenant и роль ключа, с которым открыта панель. - - +

Tenant

{session.tenant_id}

@@ -93,11 +92,9 @@ function AccessComponent() {

Роль

{session.role}

-
-
+ ) : ( - - + Не удалось определить сессию. Укажите токен в{' '} настройках @@ -106,8 +103,7 @@ function AccessComponent() { {sessionQuery.isError && sessionQuery.error instanceof Error ? ( {sessionQuery.error.message} ) : null} - - + )} {isOperator ? ( @@ -126,14 +122,12 @@ function AccessComponent() { /> ) : session ? ( - - - Управление API-ключами доступно только роли operator. Текущая роль:{' '} - {session.role}. Для выдачи ключей войдите с - operator-ключом или создайте ключ через API / переменную{' '} - EVOBGP_API_KEYS. - - + + Управление API-ключами доступно только роли operator. Текущая роль:{' '} + {session.role}. Для выдачи ключей войдите с + operator-ключом или создайте ключ через API / переменную{' '} + EVOBGP_API_KEYS. + ) : null}
) diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index b84fd84..11c1908 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -4,12 +4,7 @@ import { RefreshCw } from 'lucide-react' import { useState } from 'react' import { Button } from '@evobgp/ui/components/button' -import { - Card, - CardDescription, - CardHeader, - CardTitle, -} from '@evobgp/ui/components/card' +import { PanelCard } from '@/components/panel-card' import { Skeleton } from '@evobgp/ui/components/skeleton' import { @@ -133,13 +128,12 @@ function DashboardComponent() {
- - - Быстрые действия - Частые переходы к настройке и деплою - - - + } + footerClassName="gap-2 p-3" + />
) } diff --git a/apps/web/src/routes/_auth/firewall.tsx b/apps/web/src/routes/_auth/firewall.tsx index 9dcd9d2..e627fa1 100644 --- a/apps/web/src/routes/_auth/firewall.tsx +++ b/apps/web/src/routes/_auth/firewall.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { Button } from '@evobgp/ui/components/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +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' @@ -92,7 +92,7 @@ function FirewallPage() { toast.error( installCtx?.bundle_seed_configured === false ? 'На CP не задан EVOBGP_BUNDLE_SEED_HEX' - : 'Bundle seed недоступен (нужна роль operator)', + : 'Seed бандла недоступен (нужна роль оператора)', ) return } @@ -107,7 +107,7 @@ function FirewallPage() { return (
- - - - + + Установка на сервер - - One-liner для root на целевом Linux (bash, curl). После enroll — approve в «Запросы». - - + + } + description="Команда для root на целевом Linux (bash, curl). После регистрации — одобрите клиента во вкладке «Запросы»." + contentClassName="flex flex-col gap-4 py-4" + >
@@ -144,7 +145,7 @@ function FirewallPage() { setCpUrl(e.target.value)} />
- +

{installCtxQ.isLoading - ? 'Загрузка из control plane…' + ? 'Загрузка с плоскости управления…' : installCtx?.bundle_seed_configured ? 'Из переменной EVOBGP_BUNDLE_SEED_HEX на CP (docker compose / .env)' - : 'На CP не задан EVOBGP_BUNDLE_SEED_HEX — enroll невозможен'} + : 'На CP не задан EVOBGP_BUNDLE_SEED_HEX — регистрация невозможна'}

@@ -167,8 +168,7 @@ function FirewallPage() { Копировать команду -
-
+ - + deleteClient.mutate(id)} approvePending={approve.isPending} rejectPending={deleteClient.isPending} - emptyTitle="Нет pending-запросов" + emptyTitle="Нет ожидающих запросов" /> )} diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx index e1a925c..05825a9 100644 --- a/apps/web/src/routes/_auth/monitoring.tsx +++ b/apps/web/src/routes/_auth/monitoring.tsx @@ -3,11 +3,12 @@ import { useQuery } from '@tanstack/react-query' import { Activity, AlertTriangle, Bird, RefreshCw } from 'lucide-react' import { Button } from '@evobgp/ui/components/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { PanelCard } from '@/components/panel-card' import { Separator } from '@evobgp/ui/components/separator' import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DataGridCard } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' +import { jobKindRu } from '@/lib/ui-labels' import { DashboardOperationsFlowCard, MonitoringHealthCard, @@ -139,15 +140,16 @@ function MonitoringComponent() { - - - + BGP на API-хосте - - GET /v1/bird/status - - + + } + description="GET /v1/bird/status" + contentClassName="py-4" + > {(bird) => } - - +
- - - + Задачи - - Последние 100 задач · GET /v1/jobs - - + + } + description="Последние 100 задач · GET /v1/jobs" + contentClassName="space-y-4 py-4" + >
j.status === 'running' || j.status === 'queued').length} /> (
  • -

    {job.kind}

    +

    {jobKindRu(job.kind)}

    {job.error ? ( @@ -207,18 +209,18 @@ function MonitoringComponent() { Критичных сбоев в последних 100 задачах нет.

    )} - - + - - - + Что проверять при деградации - - Короткая шпаргалка для triage - - + + } + description="Краткая шпаргалка для первичной диагностики" + contentClassName="py-4" + >
    • API недоступен. Если{' '} @@ -226,9 +228,9 @@ function MonitoringComponent() { его логи.
    • - Readiness не «Готов». Сначала{' '} + Готовность не «Готов». Сначала{' '} postgres, затем store{' '} - и jobs в checks. + и jobs в проверках.
    • Низкий ratio BGP. Проверьте{' '} @@ -236,36 +238,37 @@ function MonitoringComponent() {
    • Ошибки задач. Откройте Операции и - проверьте последние неуспешные jobs. + проверьте последние неуспешные задачи.
    -
    -
    +
  • - - - PostgreSQL - + Статус соединения и пул. PostgreSQL отображается в readiness-проверке на вкладке «Система» (check postgres). - - - + + } + contentClassName="py-4" + /> - - - Файловые логи - + Логи API и pipeline настраиваются переменной EVOBGP_LOG_* и управляются в tenant-settings. - - - + + } + contentClassName="py-4" + />
    @@ -301,7 +304,7 @@ function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) { return (
    - Established / total + Установлено / всего {bird.bgp_established} / {bird.bgp_sessions_total} {ratio !== null ? ({ratio}%) : null} diff --git a/apps/web/src/routes/_auth/network.tsx b/apps/web/src/routes/_auth/network.tsx index 1f16598..9c44579 100644 --- a/apps/web/src/routes/_auth/network.tsx +++ b/apps/web/src/routes/_auth/network.tsx @@ -1,7 +1,7 @@ import { createFileRoute, useSearch } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { Button } from '@evobgp/ui/components/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { PanelCard } from '@/components/panel-card' import { RefreshCw } from 'lucide-react' import { @@ -72,7 +72,7 @@ function NetworkComponent() { { value: 'overview', label: 'Обзор' }, { value: 'peers', label: 'Пиры', count: peers.length }, { value: 'speakers', label: 'Спикеры', count: speakers.length, badgeVariant: 'info-light' }, - { value: 'control-plane', label: 'Control plane' }, + { value: 'control-plane', label: 'Плоскость управления' }, ]} > @@ -89,12 +89,12 @@ function NetworkComponent() { loading={overviewLoading} />
    - - - BIRD (control plane) - Статус birdc на хосте API - - + {(bird) => } - - + @@ -131,15 +130,13 @@ function NetworkComponent() { - - - Настройки Control Plane (BIRD) - Конфигурация tenant-level — в разделе «Настройки BIRD» - - - См. раздел «Настройки BIRD». - - + + См. раздел «Настройки BIRD». +
    diff --git a/apps/web/src/routes/_auth/operations.tsx b/apps/web/src/routes/_auth/operations.tsx index 8175b95..0c8c5da 100644 --- a/apps/web/src/routes/_auth/operations.tsx +++ b/apps/web/src/routes/_auth/operations.tsx @@ -5,7 +5,7 @@ import { toast } from 'sonner' import { useState, useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { PanelCard } from '@/components/panel-card' import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DataGridCard } from '@/components/data-grid-shell' import { OperationsAnalyticsCard } from '@/components/analytics' @@ -97,22 +97,22 @@ function OperationsComponent() { - Apply + Применить } title="Применить конфигурацию на всех спикерах?" - description="Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль operator." + description="Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль оператора." confirmLabel="Применить" onConfirm={() => applyMutation.mutate()} /> - BIRD reload + Перезагрузка BIRD } title="Перезагрузить BIRD?" - description="BIRD перезагрузит конфигурацию. Требуется роль operator." + description="BIRD перезагрузит конфигурацию. Требуется роль оператора." confirmLabel="Перезагрузить" onConfirm={() => birdReloadMutation.mutate()} /> @@ -202,11 +202,7 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[] ) return ( - - - Сравнение ревизий - - +
    Ревизия A @@ -242,8 +238,7 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[] > {(diff) => } - - + ) } diff --git a/apps/web/src/routes/_auth/schedule.tsx b/apps/web/src/routes/_auth/schedule.tsx index 88bff49..5d4749d 100644 --- a/apps/web/src/routes/_auth/schedule.tsx +++ b/apps/web/src/routes/_auth/schedule.tsx @@ -39,7 +39,7 @@ function ScheduleComponent() { const items: SectionCardItem[] = [ { label: 'Всего задач', value: jobs.length, icon: , hint: 'в выборке' }, - { label: 'В работе', value: running, icon: , hint: 'queued и running' }, + { label: 'В работе', value: running, icon: , hint: 'в очереди и выполняются' }, { label: 'С ошибкой', value: failed, @@ -90,7 +90,7 @@ function ScheduleComponent() { - - - Подключение к API - - Токен хранится только в этом браузере (localStorage). Управление ключами tenant — в - разделе «Права доступа». - - - +
    EVOBGP_SEED_DEMO ≠ 0) и запущенный API.

    ) : null} - - + - - - Оформление - - Тема интерфейса. Быстрый переключатель также доступен в боковой панели. - - - + v && setTheme(v)} /> - - +
    ) } diff --git a/apps/web/src/routes/_auth/tenant-settings.tsx b/apps/web/src/routes/_auth/tenant-settings.tsx index c944887..712244d 100644 --- a/apps/web/src/routes/_auth/tenant-settings.tsx +++ b/apps/web/src/routes/_auth/tenant-settings.tsx @@ -4,7 +4,7 @@ import { Save } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +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' @@ -42,8 +42,8 @@ const RUNTIME_LOGS_ENABLED_ITEMS = [ ] as const const RUNTIME_LOGS_MODE_ITEMS = [ - { value: 'truncate', label: 'truncate — обнулить' }, - { value: 'delete', label: 'delete — удалить файл' }, + { value: 'truncate', label: 'обнулить (truncate)' }, + { value: 'delete', label: 'удалить файл (delete)' }, ] as const const BIRD_LABELS: Record = { @@ -99,8 +99,8 @@ function TenantSettingsComponent() { return (
    - - - BIRD control plane - + Глобальные параметры BIRD для pipeline refresh/apply. Сохранение через{' '} PATCH /v1/settings (роль operator). - - - + + } + contentClassName="space-y-4 py-4" + > )} - - + - - - Ревизии - Время хранения ревизий в БД - - + )} - - + - - - Файловые логи - Автоматическая очистка логов - - + )} - - + - {children} - - - - - ) -} - function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) { return ( ) } +function Progress({ + className, + children, + value, + ...props +}: ProgressPrimitive.Root.Props) { + const hasCustomTrack = React.Children.toArray(children).some( + (child) => React.isValidElement(child) && child.type === ProgressTrack, + ) + + return ( + + {children} + {!hasCustomTrack ? ( + + + + ) : null} + + ) +} + function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { return (