+
) : 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"
+ >
@@ -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 — в
- разделе «Права доступа».
-
-
-
+
)
}
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 (