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 (