fix(web): update Russian translations for various components
Обновлены переводы на русский язык для компонентов, включая карточки мониторинга, сетевые панели и настройки. Исправлены описания и метки для улучшения пользовательского интерфейса, а также добавлены новые элементы для поддержки локализации в компонентах, таких как DataGrid и ResourcePage.
This commit is contained in:
@@ -27,10 +27,10 @@ export function MonitoringHealthCard({
|
||||
return (
|
||||
<DonutBreakdownCard
|
||||
title="Доступность системы"
|
||||
description="GET /v1/health · GET /v1/ready"
|
||||
description="Проверки живучести и готовности"
|
||||
slices={slices}
|
||||
centerLabel="Проверки"
|
||||
badge={healthOk ? 'API OK' : undefined}
|
||||
badge={healthOk ? 'API в норме' : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@ export function NetworkOverviewAnalyticsCard({
|
||||
/>
|
||||
<SegmentedProgressCard
|
||||
title="Спикеры"
|
||||
description="Доступность live-агентов"
|
||||
description="Доступность агентов"
|
||||
primary={{
|
||||
value: `${net.speakersOnline}/${net.speakersTotal}`,
|
||||
label: 'Online',
|
||||
label: 'В сети',
|
||||
percent: speakersPct,
|
||||
}}
|
||||
secondary={{
|
||||
value: net.speakersTotal - net.speakersOnline,
|
||||
label: 'Offline',
|
||||
label: 'Не в сети',
|
||||
percent: 100 - speakersPct,
|
||||
}}
|
||||
footer={`Пиры установлены: ${net.peersEstablished}/${net.peersEnabled}`}
|
||||
|
||||
@@ -100,7 +100,7 @@ function buildKpis({
|
||||
variant={network.speakersOnline === network.speakersTotal ? 'success-light' : 'warning-light'}
|
||||
size="sm"
|
||||
>
|
||||
{loading ? '…' : 'онлайн'}
|
||||
{loading ? '…' : 'в сети'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ function buildMetrics({
|
||||
{
|
||||
id: 'bgp',
|
||||
title: 'BGP готовность',
|
||||
label: 'Established / включённые',
|
||||
label: 'Установлено / включено',
|
||||
value: loading || network.peersEnabled === 0 ? '—' : `${bgpPct}%`,
|
||||
delta: loading ? '…' : bgpPct >= 90 ? 'стабильно' : 'внимание',
|
||||
deltaVariant: bgpPct >= 90 ? 'success-light' : bgpPct >= 50 ? 'warning-light' : 'destructive-light',
|
||||
@@ -62,13 +62,13 @@ function buildMetrics({
|
||||
{
|
||||
id: 'speakers',
|
||||
title: 'Спикеры',
|
||||
label: 'Online / всего',
|
||||
label: 'В сети / всего',
|
||||
value: loading ? '—' : `${network.speakersOnline}/${network.speakersTotal}`,
|
||||
delta:
|
||||
loading || network.speakersTotal === 0
|
||||
? '…'
|
||||
: network.speakersOnline === network.speakersTotal
|
||||
? 'все online'
|
||||
? 'все в сети'
|
||||
: 'частично',
|
||||
deltaVariant:
|
||||
network.speakersOnline === network.speakersTotal ? 'success-light' : 'warning-light',
|
||||
|
||||
@@ -7,7 +7,7 @@ const ACTIONS: QuickActionItem[] = [
|
||||
{
|
||||
id: 'lookup',
|
||||
title: 'Проверка IP/домена',
|
||||
description: 'Проверка IP в списках и BGP-сообществах.',
|
||||
description: 'Проверка IP в списках и BGP community.',
|
||||
to: '/lookup',
|
||||
icon: <Search aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
@@ -22,8 +22,8 @@ const ACTIONS: QuickActionItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'communities',
|
||||
title: 'BGP-сообщества',
|
||||
description: 'Справочник BGP-сообществ для политик экспорта.',
|
||||
title: 'BGP community',
|
||||
description: 'Справочник BGP community для политик экспорта.',
|
||||
to: '/directories',
|
||||
icon: <Tags aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
|
||||
@@ -71,7 +71,7 @@ export function CommunityFormDialog({
|
||||
<FormDrawer
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={editTarget ? 'Редактировать сообщество' : 'Новое сообщество BGP'}
|
||||
title={editTarget ? 'Редактировать community' : 'Новое BGP community'}
|
||||
description="Тег для префиксов в фильтрах BIRD"
|
||||
className="sm:max-w-sm"
|
||||
footer={
|
||||
|
||||
@@ -73,10 +73,10 @@ export function DirectoriesCommunitiesGrid({
|
||||
table={table}
|
||||
recordCount={filteredCount}
|
||||
isLoading={isLoading}
|
||||
emptyMessage="Нет сообществ"
|
||||
emptyMessage="Нет community"
|
||||
searchValue={globalFilter}
|
||||
onSearchChange={setGlobalFilter}
|
||||
searchPlaceholder="Поиск сообществ…"
|
||||
searchPlaceholder="Поиск community…"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function DohProfileFormDialog({
|
||||
if (timeoutMs.trim() !== '') {
|
||||
const ms = Number(timeoutMs)
|
||||
if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
|
||||
toast.error('Timeout должен быть целым числом > 0')
|
||||
toast.error('Таймаут должен быть целым числом больше 0')
|
||||
return
|
||||
}
|
||||
timeout = ms
|
||||
@@ -136,7 +136,7 @@ export function DohProfileFormDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="doh-timeout">Timeout, мс (опционально)</Label>
|
||||
<Label htmlFor="doh-timeout">Таймаут, мс (необязательно)</Label>
|
||||
<Input
|
||||
id="doh-timeout"
|
||||
type="number"
|
||||
|
||||
@@ -71,7 +71,7 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
to: '/dashboard',
|
||||
label: 'Панель',
|
||||
icon: LayoutDashboard,
|
||||
description: 'KPI, модули и активность',
|
||||
description: 'Метрики, модули и активность',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -81,15 +81,15 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
{ to: '/modules', label: 'Модули', icon: Boxes, description: 'Списки префиксов и AS' },
|
||||
{ to: '/lookup', label: 'Проверка', icon: Search, description: 'IP/домен в списках и community' },
|
||||
{ to: '/network', label: 'Сеть', icon: Network, description: 'BGP-пиры и спикеры', search: { tab: 'overview' } },
|
||||
{ to: '/directories', label: 'Справочники', icon: BookText, description: 'Communities и DoH' },
|
||||
{ to: '/directories', label: 'Справочники', icon: BookText, description: 'BGP community и DoH' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Операции',
|
||||
items: [
|
||||
{ to: '/operations', label: 'Операции', icon: Cog, description: 'Ревизии и apply', search: { tab: 'revisions' } },
|
||||
{ to: '/schedule', label: 'Задачи', icon: ListChecks, description: 'Расписание refresh' },
|
||||
{ to: '/monitoring', label: 'Мониторинг', icon: Activity, description: 'Health и BIRD', search: { tab: 'system' } },
|
||||
{ to: '/operations', label: 'Операции', icon: Cog, description: 'Ревизии и применение', search: { tab: 'revisions' } },
|
||||
{ to: '/schedule', label: 'Задачи', icon: ListChecks, description: 'Расписание обновления' },
|
||||
{ to: '/monitoring', label: 'Мониторинг', icon: Activity, description: 'Состояние системы и BIRD', search: { tab: 'system' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -94,8 +94,8 @@ export function SystemMonitorPopover() {
|
||||
() => [
|
||||
{
|
||||
id: 'api',
|
||||
label: 'API health',
|
||||
value: healthOk ? 'OK' : '—',
|
||||
label: 'Состояние API',
|
||||
value: healthOk ? 'норма' : '—',
|
||||
unit: '',
|
||||
percent: healthOk ? 100 : 0,
|
||||
icon: <HeartPulse aria-hidden />,
|
||||
|
||||
@@ -14,8 +14,8 @@ import { normalizeCdnSourceKind } from '@/lib/modules/helpers'
|
||||
import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api'
|
||||
|
||||
const CDN_KIND_ITEMS = [
|
||||
{ value: 'plaintext', label: 'plaintext' },
|
||||
{ value: 'json', label: 'json' },
|
||||
{ value: 'plaintext', label: 'Текст' },
|
||||
{ value: 'json', label: 'JSON' },
|
||||
] as const
|
||||
|
||||
interface ModuleCdnSourceDialogProps {
|
||||
|
||||
@@ -160,7 +160,7 @@ export function ModuleEditDialog({
|
||||
<div className="grid min-w-0 flex-1 gap-1 pr-2">
|
||||
<Label htmlFor="mod-enabled">Включён</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Выключенный модуль не участвует в refresh и apply.
|
||||
Выключенный модуль не участвует в обновлении и применении.
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { formatDateTime } from '@/lib/modules/display'
|
||||
import { communityLabel } from '@/lib/modules/helpers'
|
||||
import { cdnSourceKindRu } from '@/lib/ui-labels'
|
||||
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
|
||||
import type {
|
||||
AsEntry,
|
||||
@@ -145,7 +146,7 @@ export function ModuleEntriesGrid({
|
||||
accessorKey: 'source_kind',
|
||||
header: 'Тип',
|
||||
cell: ({ row }: { row: { original: CdnSource } }) => (
|
||||
<CategoryBadge>{row.original.source_kind}</CategoryBadge>
|
||||
<CategoryBadge>{cdnSourceKindRu(row.original.source_kind)}</CategoryBadge>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ import type { PeerDiscoveryRow, SpeakerRow } from '@/types/api'
|
||||
function speakerLabel(s: SpeakerRow): string {
|
||||
if (s.role === 'master') {
|
||||
const host = s.agent_domain ?? s.endpoint
|
||||
return host ? `CP · ${host}` : 'CP (master)'
|
||||
return host ? `Плоскость · ${host}` : 'Плоскость управления'
|
||||
}
|
||||
return s.agent_domain ?? s.endpoint ?? `${s.id.slice(0, 8)}…`
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const filterFields: FilterFieldConfig[] = [
|
||||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||||
type: 'text',
|
||||
className: 'w-48',
|
||||
placeholder: 'IP или Neighbor ID…',
|
||||
placeholder: 'IP или ID соседа…',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -94,7 +94,7 @@ export function NetworkDiscoveredPeersCard({
|
||||
id: 'neighbor_id',
|
||||
accessorFn: (row) => row.neighbor_id || row.neighbor,
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Neighbor ID" />
|
||||
<DataGridColumnHeader column={column} title="ID соседа" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<DataGridPrimaryCell
|
||||
@@ -103,7 +103,7 @@ export function NetworkDiscoveredPeersCard({
|
||||
accent="mono"
|
||||
/>
|
||||
),
|
||||
meta: { headerTitle: 'Neighbor ID' },
|
||||
meta: { headerTitle: 'ID соседа' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'remote_asn',
|
||||
@@ -212,7 +212,7 @@ export function NetworkDiscoveredPeersCard({
|
||||
title="Одобрить пира"
|
||||
description={
|
||||
approveTarget
|
||||
? `Neighbor ID ${approveTarget.neighbor_id || '—'} · ${approveTarget.neighbor} AS${approveTarget.remote_asn ?? '?'}`
|
||||
? `ID соседа ${approveTarget.neighbor_id || '—'} · ${approveTarget.neighbor} AS${approveTarget.remote_asn ?? '?'}`
|
||||
: undefined
|
||||
}
|
||||
className="sm:max-w-sm"
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NetworkKpi({
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{loading ? '…' : 'онлайн'}
|
||||
{loading ? '…' : 'в сети'}
|
||||
</Badge>
|
||||
),
|
||||
to: '/network',
|
||||
|
||||
@@ -39,11 +39,11 @@ const PEER_TABS = [
|
||||
|
||||
const SESSION_STATE_OPTIONS = [
|
||||
{ value: 'Established', label: 'Установлена' },
|
||||
{ value: 'Idle', label: 'Idle' },
|
||||
{ value: 'Active', label: 'Active' },
|
||||
{ value: 'Connect', label: 'Connect' },
|
||||
{ value: 'OpenSent', label: 'OpenSent' },
|
||||
{ value: 'OpenConfirm', label: 'OpenConfirm' },
|
||||
{ value: 'Idle', label: 'Простой' },
|
||||
{ value: 'Active', label: 'Поиск' },
|
||||
{ value: 'Connect', label: 'Соединение' },
|
||||
{ value: 'OpenSent', label: 'Open отправлен' },
|
||||
{ value: 'OpenConfirm', label: 'Open подтверждён' },
|
||||
]
|
||||
|
||||
function createDefaultPeerFilters(): Filter[] {
|
||||
|
||||
@@ -31,14 +31,14 @@ function speakerDeleteLabel(s: SpeakerRow): string {
|
||||
|
||||
const SPEAKER_TABS = [
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'online', label: 'Online' },
|
||||
{ id: 'offline', label: 'Offline' },
|
||||
{ id: 'online', label: 'В сети' },
|
||||
{ id: 'offline', label: 'Не в сети' },
|
||||
]
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: 'primary', label: 'primary' },
|
||||
{ value: 'secondary', label: 'secondary' },
|
||||
{ value: 'speaker', label: 'speaker' },
|
||||
{ value: 'primary', label: 'Основной' },
|
||||
{ value: 'secondary', label: 'Резервный' },
|
||||
{ value: 'speaker', label: 'Спикер' },
|
||||
]
|
||||
|
||||
function createDefaultSpeakerFilters(): Filter[] {
|
||||
@@ -52,7 +52,7 @@ const speakerFilterFields: FilterFieldConfig[] = [
|
||||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||||
type: 'text',
|
||||
className: 'w-52',
|
||||
placeholder: 'endpoint…',
|
||||
placeholder: 'Адрес агента…',
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
@@ -128,7 +128,7 @@ export function NetworkSpeakersCard({
|
||||
<>
|
||||
<ResourcePage
|
||||
title="Спикеры"
|
||||
description="BIRD-агенты на нодах tenant"
|
||||
description="BIRD-агенты на нодах арендатора"
|
||||
tabs={SPEAKER_TABS}
|
||||
tabFilter={speakerTabFilter}
|
||||
filterFields={speakerFilterFields}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DataGridPrimaryCell } from '@/components/data-grid-cell'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { speakerOnlineLabel } from '@/lib/ui-labels'
|
||||
import { speakerOnlineLabel, speakerRoleRu } from '@/lib/ui-labels'
|
||||
import type { SpeakerRow } from '@/types/api'
|
||||
|
||||
export const speakerColumns: ColumnDef<SpeakerRow, unknown>[] = [
|
||||
@@ -20,7 +20,7 @@ export const speakerColumns: ColumnDef<SpeakerRow, unknown>[] = [
|
||||
{
|
||||
accessorKey: 'role',
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Роль" />,
|
||||
cell: ({ row }) => <CategoryBadge>{row.original.role}</CategoryBadge>,
|
||||
cell: ({ row }) => <CategoryBadge>{speakerRoleRu(row.original.role)}</CategoryBadge>,
|
||||
meta: { headerTitle: 'Роль' },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ interface PeerFormDialogProps {
|
||||
function speakerLabel(s: SpeakerRow): string {
|
||||
if (s.role === 'master') {
|
||||
const host = s.agent_domain ?? s.endpoint
|
||||
return host ? `CP · ${host}` : 'CP (master)'
|
||||
return host ? `Плоскость · ${host}` : 'Плоскость управления'
|
||||
}
|
||||
return s.agent_domain ?? s.endpoint ?? `${s.id.slice(0, 8)}…`
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
||||
const ep =
|
||||
endpoint.trim() || (agentDomain.trim() ? `https://${agentDomain.trim()}` : '')
|
||||
if (!ep) {
|
||||
toast.error('Укажите endpoint или agent domain')
|
||||
toast.error('Укажите конечную точку или домен агента')
|
||||
return
|
||||
}
|
||||
const body: BgpSpeakerCreate = {
|
||||
@@ -95,7 +95,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title="Новый спикер"
|
||||
description="BIRD-агент на ноде реплики или control plane"
|
||||
description="BIRD-агент на ноде реплики или плоскости управления"
|
||||
className="sm:max-w-md"
|
||||
footer={
|
||||
<>
|
||||
@@ -122,7 +122,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
||||
label="Роль"
|
||||
items={[
|
||||
{ value: 'replica', label: 'Реплика' },
|
||||
{ value: 'master', label: 'Мастер (CP)' },
|
||||
{ value: 'master', label: 'Мастер (плоскость)' },
|
||||
]}
|
||||
value={role}
|
||||
onValueChange={(v) => setRole(v ?? 'replica')}
|
||||
@@ -146,7 +146,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="speaker-bgp-source">BGP source IPv4</Label>
|
||||
<Label htmlFor="speaker-bgp-source">Исходный IPv4 BGP</Label>
|
||||
<Input
|
||||
id="speaker-bgp-source"
|
||||
placeholder="203.0.113.10"
|
||||
|
||||
@@ -77,7 +77,7 @@ export function OperationsRevisionsGrid({
|
||||
</Button>
|
||||
}
|
||||
title={`Откатиться к ревизии ${row.original.id.slice(0, 8)}…?`}
|
||||
description="Будет создана новая ревизия на основе выбранной. Требуется роль operator."
|
||||
description="Будет создана новая ревизия на основе выбранной. Требуется роль оператора."
|
||||
confirmLabel="Откатить"
|
||||
destructive
|
||||
onConfirm={() => rollbackMutation.mutate(row.original.id)}
|
||||
|
||||
@@ -61,7 +61,7 @@ function DataGridColumnFilter<TData, TValue>({
|
||||
<div className="hidden space-x-1 lg:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge variant="secondary" className="px-1 font-normal">
|
||||
{selectedValues.size} selected
|
||||
{selectedValues.size} выбрано
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
@@ -94,7 +94,7 @@ function DataGridColumnFilter<TData, TValue>({
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<div className="text-muted-foreground py-6 text-center text-sm">
|
||||
No results found.
|
||||
Ничего не найдено.
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1">
|
||||
@@ -170,7 +170,7 @@ function DataGridColumnFilter<TData, TValue>({
|
||||
}}
|
||||
className="hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground rounded-md relative flex cursor-pointer items-center justify-center px-2 py-1.5 text-sm outline-hidden select-none"
|
||||
>
|
||||
Clear filters
|
||||
Сбросить фильтры
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -39,11 +39,11 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element {
|
||||
sizes: [5, 10, 25, 50, 100],
|
||||
sizesSkeleton: <Skeleton className="h-8 w-44" />,
|
||||
moreLimit: 5,
|
||||
info: "{from} - {to} of {count}",
|
||||
info: "{from}–{to} из {count}",
|
||||
infoSkeleton: <Skeleton className="h-8 w-60" />,
|
||||
rowsPerPageLabel: "Rows per page",
|
||||
previousPageLabel: "Go to previous page",
|
||||
nextPageLabel: "Go to next page",
|
||||
rowsPerPageLabel: "Строк на странице",
|
||||
previousPageLabel: "Предыдущая страница",
|
||||
nextPageLabel: "Следующая страница",
|
||||
ellipsisText: "...",
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ function DataGridTableDndRowHandle({ className }: { className?: string }) {
|
||||
"size-7 cursor-grab opacity-70 hover:bg-transparent hover:opacity-100 active:cursor-grabbing",
|
||||
className
|
||||
)}
|
||||
aria-label="Drag to reorder row"
|
||||
aria-label="Перетащить строку"
|
||||
disabled
|
||||
>
|
||||
<GripHorizontalIcon aria-hidden="true" />
|
||||
@@ -89,7 +89,7 @@ function DataGridTableDndRowHandle({ className }: { className?: string }) {
|
||||
"size-7 cursor-grab opacity-70 hover:bg-transparent hover:opacity-100 active:cursor-grabbing",
|
||||
className
|
||||
)}
|
||||
aria-label="Drag to reorder row"
|
||||
aria-label="Перетащить строку"
|
||||
{...context.attributes}
|
||||
{...context.listeners}
|
||||
>
|
||||
|
||||
@@ -105,7 +105,7 @@ function DataGridTableDndHeader<TData>({
|
||||
className={`-ms-2 size-6 ${isDragging ? "cursor-grabbing" : "cursor-grab active:cursor-grabbing"}`}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label="Drag to reorder"
|
||||
aria-label="Перетащить для изменения порядка"
|
||||
>
|
||||
<GripVerticalIcon className="opacity-60 hover:opacity-100" aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
@@ -397,9 +397,9 @@ function DataGridTableVirtual<TData>({
|
||||
|
||||
const isVirtualizationEnabled = virtualizerOptions?.enabled !== false
|
||||
const loadingMoreMessage =
|
||||
props.fetchingMoreMessage || props.loadingMessage || "Loading..."
|
||||
props.fetchingMoreMessage || props.loadingMessage || "Загрузка…"
|
||||
const allRowsLoadedMessage =
|
||||
props.allRowsLoadedMessage || "All records loaded"
|
||||
props.allRowsLoadedMessage || "Все записи загружены"
|
||||
|
||||
const handleViewportRef = useCallback((node: HTMLDivElement | null) => {
|
||||
setViewportElements({
|
||||
|
||||
@@ -1241,7 +1241,7 @@ function DataGridTableEmpty() {
|
||||
colSpan={Math.max(visibleColumnCount, 1)}
|
||||
className="text-muted-foreground py-6 text-center text-sm"
|
||||
>
|
||||
{props.emptyMessage || "No data available"}
|
||||
{props.emptyMessage || "Нет данных"}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
@@ -1254,7 +1254,7 @@ function DataGridTableLoader() {
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="text-muted-foreground bg-card rounded-lg flex items-center gap-2 border px-4 py-2 text-sm leading-none font-medium">
|
||||
<Spinner className="size-5 opacity-60" />
|
||||
{props.loadingMessage || "Loading..."}
|
||||
{props.loadingMessage || "Загрузка…"}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1266,7 +1266,7 @@ function DataGridTableRowPin<TData>({ row }: { row: Row<TData> }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isPinned ? "Unpin row" : "Pin row"}
|
||||
aria-label={isPinned ? "Открепить строку" : "Закрепить строку"}
|
||||
onClick={() => {
|
||||
if (isPinned) {
|
||||
row.pin(false)
|
||||
@@ -1322,7 +1322,7 @@ function DataGridTableRowSelect<TData>({ row }: { row: Row<TData> }) {
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
aria-label="Выбрать строку"
|
||||
className="align-[inherit]"
|
||||
/>
|
||||
</>
|
||||
@@ -1341,7 +1341,7 @@ function DataGridTableRowSelectAll() {
|
||||
indeterminate={isSomeSelected && !isAllSelected}
|
||||
disabled={isLoading || recordCount === 0}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
aria-label="Выбрать все"
|
||||
className="align-[inherit]"
|
||||
/>
|
||||
)
|
||||
@@ -1408,7 +1408,7 @@ function DataGridTableBodyRows<TData>({ table }: { table: Table<TData> }) {
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{props.loadingMessage || "Loading..."}
|
||||
{props.loadingMessage || "Загрузка…"}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -123,75 +123,70 @@ export interface FilterI18nConfig {
|
||||
|
||||
// Default English i18n configuration
|
||||
export const DEFAULT_I18N: FilterI18nConfig = {
|
||||
// UI Labels
|
||||
addFilter: "Filter",
|
||||
searchFields: "Filter...",
|
||||
noFieldsFound: "No filters found.",
|
||||
noResultsFound: "No results found.",
|
||||
select: "Select...",
|
||||
true: "True",
|
||||
false: "False",
|
||||
min: "Min",
|
||||
max: "Max",
|
||||
to: "to",
|
||||
typeAndPressEnter: "Type and press Enter to add tag",
|
||||
selected: "selected",
|
||||
selectedCount: "selected",
|
||||
addFilter: "Фильтр",
|
||||
searchFields: "Фильтр…",
|
||||
noFieldsFound: "Поля не найдены.",
|
||||
noResultsFound: "Ничего не найдено.",
|
||||
select: "Выбрать…",
|
||||
true: "Да",
|
||||
false: "Нет",
|
||||
min: "Мин.",
|
||||
max: "Макс.",
|
||||
to: "—",
|
||||
typeAndPressEnter: "Введите и нажмите Enter",
|
||||
selected: "выбрано",
|
||||
selectedCount: "выбрано",
|
||||
percent: "%",
|
||||
defaultCurrency: "$",
|
||||
defaultColor: "#000000",
|
||||
addFilterTitle: "Add filter",
|
||||
addFilterTitle: "Добавить фильтр",
|
||||
|
||||
// Operators
|
||||
operators: {
|
||||
is: "is",
|
||||
isNot: "is not",
|
||||
isAnyOf: "is any of",
|
||||
isNotAnyOf: "is not any of",
|
||||
includesAll: "includes all",
|
||||
excludesAll: "excludes all",
|
||||
before: "before",
|
||||
after: "after",
|
||||
between: "between",
|
||||
notBetween: "not between",
|
||||
contains: "contains",
|
||||
notContains: "does not contain",
|
||||
startsWith: "starts with",
|
||||
endsWith: "ends with",
|
||||
isExactly: "is exactly",
|
||||
equals: "equals",
|
||||
notEquals: "not equals",
|
||||
greaterThan: "greater than",
|
||||
lessThan: "less than",
|
||||
overlaps: "overlaps",
|
||||
includes: "includes",
|
||||
excludes: "excludes",
|
||||
includesAllOf: "includes all of",
|
||||
includesAnyOf: "includes any of",
|
||||
empty: "is empty",
|
||||
notEmpty: "is not empty",
|
||||
is: "равно",
|
||||
isNot: "не равно",
|
||||
isAnyOf: "любое из",
|
||||
isNotAnyOf: "кроме",
|
||||
includesAll: "включает все",
|
||||
excludesAll: "исключает все",
|
||||
before: "до",
|
||||
after: "после",
|
||||
between: "между",
|
||||
notBetween: "вне диапазона",
|
||||
contains: "содержит",
|
||||
notContains: "не содержит",
|
||||
startsWith: "начинается с",
|
||||
endsWith: "заканчивается на",
|
||||
isExactly: "точно",
|
||||
equals: "равно",
|
||||
notEquals: "не равно",
|
||||
greaterThan: "больше",
|
||||
lessThan: "меньше",
|
||||
overlaps: "пересекается",
|
||||
includes: "включает",
|
||||
excludes: "исключает",
|
||||
includesAllOf: "включает все",
|
||||
includesAnyOf: "включает любое",
|
||||
empty: "пусто",
|
||||
notEmpty: "не пусто",
|
||||
},
|
||||
|
||||
// Placeholders
|
||||
placeholders: {
|
||||
enterField: (fieldType: string) => `Enter ${fieldType}...`,
|
||||
selectField: "Select...",
|
||||
searchField: (fieldName: string) => `Search ${fieldName.toLowerCase()}...`,
|
||||
enterKey: "Enter key...",
|
||||
enterValue: "Enter value...",
|
||||
enterField: (fieldType: string) => `Введите ${fieldType}…`,
|
||||
selectField: "Выбрать…",
|
||||
searchField: (fieldName: string) => `Поиск ${fieldName.toLowerCase()}…`,
|
||||
enterKey: "Введите ключ…",
|
||||
enterValue: "Введите значение…",
|
||||
},
|
||||
|
||||
// Helper functions
|
||||
helpers: {
|
||||
formatOperator: (operator: string) => operator.replace(/_/g, " "),
|
||||
},
|
||||
|
||||
// Validation
|
||||
validation: {
|
||||
invalidEmail: "Invalid email format",
|
||||
invalidUrl: "Invalid URL format",
|
||||
invalidTel: "Invalid phone format",
|
||||
invalid: "Invalid input format",
|
||||
invalidEmail: "Некорректный email",
|
||||
invalidUrl: "Некорректный URL",
|
||||
invalidTel: "Некорректный телефон",
|
||||
invalid: "Некорректное значение",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export function ScheduleAgendaPanel({
|
||||
return (
|
||||
<PanelCard
|
||||
title="Календарь задач"
|
||||
description="Задачи refresh и apply по дням"
|
||||
description="Задачи обновления и применения по дням"
|
||||
contentClassName={cn(panelCardContentFlushClassName, 'p-0')}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ScheduleModulesGrid({
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{row.original.cron_expr ??
|
||||
(row.original.refresh_interval_sec ? `${row.original.refresh_interval_sec}s` : '—')}
|
||||
(row.original.refresh_interval_sec ? `${row.original.refresh_interval_sec} с` : '—')}
|
||||
</span>
|
||||
),
|
||||
meta: { headerTitle: 'Расписание' },
|
||||
|
||||
@@ -90,16 +90,16 @@ export function AppearanceSettingsTab() {
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard
|
||||
title="Дашборд"
|
||||
title="Обзор"
|
||||
description="Блоки на экране «Обзор»"
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Быстрые действия"
|
||||
description="Показывать KPI-like плитки быстрых переходов под метриками."
|
||||
description="Показывать плитки быстрых переходов под метриками."
|
||||
>
|
||||
<SettingRow
|
||||
title="Быстрые действия"
|
||||
description="Блок с частыми переходами (модули, сеть, деплой) на дашборде."
|
||||
description="Блок с частыми переходами (модули, сеть, деплой) на экране «Обзор»."
|
||||
last
|
||||
>
|
||||
<Switch
|
||||
|
||||
@@ -31,8 +31,8 @@ const RUNTIME_LOGS_ENABLED_ITEMS = [
|
||||
] as const
|
||||
|
||||
const RUNTIME_LOGS_MODE_ITEMS = [
|
||||
{ value: 'truncate', label: 'обнулить (truncate)' },
|
||||
{ value: 'delete', label: 'удалить файл (delete)' },
|
||||
{ value: 'truncate', label: 'Обнулить файл' },
|
||||
{ value: 'delete', label: 'Удалить файл' },
|
||||
] as const
|
||||
|
||||
const BIRD_LABELS: Record<BirdSettingKey, string> = {
|
||||
@@ -40,12 +40,12 @@ const BIRD_LABELS: Record<BirdSettingKey, string> = {
|
||||
bird_local_ipv4: 'Локальный IPv4',
|
||||
bird_local_ipv6: 'Локальный IPv6',
|
||||
bird_local_asn: 'Локальный ASN',
|
||||
bird_bgp_source_ipv4: 'BGP source IPv4',
|
||||
bird_bgp_source_ipv6: 'BGP source IPv6',
|
||||
bird_bgp_source_ipv4: 'Исходный IPv4 BGP',
|
||||
bird_bgp_source_ipv6: 'Исходный IPv6 BGP',
|
||||
peer_discovery_enabled: 'Автообнаружение пиров',
|
||||
peer_discovery_ranges_v4: 'Discovery CIDR IPv4',
|
||||
peer_discovery_ranges_v6: 'Discovery CIDR IPv6',
|
||||
peer_discovery_require_external: 'Только external ASN',
|
||||
peer_discovery_ranges_v4: 'CIDR автообнаружения IPv4',
|
||||
peer_discovery_ranges_v6: 'CIDR автообнаружения IPv6',
|
||||
peer_discovery_require_external: 'Только внешние ASN',
|
||||
}
|
||||
|
||||
const BIRD_BOOL_ITEMS = [
|
||||
@@ -55,10 +55,10 @@ const BIRD_BOOL_ITEMS = [
|
||||
|
||||
const BIRD_HINTS: Partial<Record<BirdSettingKey, string>> = {
|
||||
peer_discovery_enabled:
|
||||
'Dynamic neighbor range в BIRD (карантин import/export none). Требует CIDR.',
|
||||
'Динамический диапазон соседей в BIRD (карантин import/export none). Нужны CIDR.',
|
||||
peer_discovery_ranges_v4: 'Через запятую или пробел, напр. 198.51.100.0/24 203.0.113.0/24',
|
||||
peer_discovery_ranges_v6: 'Опционально, напр. 2001:db8::/32',
|
||||
peer_discovery_require_external: 'neighbor range … external (любой чужой ASN)',
|
||||
peer_discovery_require_external: 'Диапазон соседей с внешним (любым чужим) ASN',
|
||||
}
|
||||
|
||||
export function BirdSettingsTab() {
|
||||
@@ -103,8 +103,8 @@ export function BirdSettingsTab() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsCard
|
||||
title="BIRD control plane"
|
||||
description="Глобальные параметры BIRD для pipeline refresh/apply. Сохранение — роль operator."
|
||||
title="Плоскость управления BIRD"
|
||||
description="Глобальные параметры BIRD для обновления модулей и применения конфигурации. Сохранение доступно оператору."
|
||||
footer={
|
||||
<LoadingButton onClick={saveBird} loading={patchMutation.isPending}>
|
||||
<Save />
|
||||
@@ -184,11 +184,11 @@ export function BirdSettingsTab() {
|
||||
>
|
||||
{() => (
|
||||
<SettingsFieldGroup
|
||||
legend="Retention"
|
||||
legend="Хранение"
|
||||
description="Срок хранения ревизий в минутах."
|
||||
>
|
||||
<SettingsSettingField
|
||||
title="Retention"
|
||||
title="Срок хранения"
|
||||
description="Сколько минут хранить ревизии конфигурации."
|
||||
labelFor="revision_retention_minutes"
|
||||
stacked
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ConnectionSettingsTab({ tokenRequired }: { tokenRequired: boolea
|
||||
|
||||
<SettingsCard
|
||||
title="Подключение к API"
|
||||
description="Токен хранится только в этом браузере (localStorage)"
|
||||
description="Токен хранится только в этом браузере"
|
||||
footer={
|
||||
<div className="flex w-full min-w-0 flex-wrap justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={useDevToken}>
|
||||
@@ -84,7 +84,7 @@ export function ConnectionSettingsTab({ tokenRequired }: { tokenRequired: boolea
|
||||
title="Токен для запросов"
|
||||
description={
|
||||
<>
|
||||
Ключ для заголовка Authorization. Управление ключами tenant — в разделе{' '}
|
||||
Ключ для заголовка Authorization. Управление ключами арендатора — в разделе{' '}
|
||||
<Link to="/access" className="text-primary underline-offset-4 hover:underline">
|
||||
Права доступа
|
||||
</Link>
|
||||
@@ -93,7 +93,7 @@ export function ConnectionSettingsTab({ tokenRequired }: { tokenRequired: boolea
|
||||
}
|
||||
titleAddon={
|
||||
<Badge variant="outline" size="sm">
|
||||
localStorage
|
||||
браузер
|
||||
</Badge>
|
||||
}
|
||||
labelFor="settings-token"
|
||||
|
||||
@@ -28,25 +28,25 @@ const ADMIN_SECTIONS = [
|
||||
title: 'Параметры арендатора',
|
||||
description: 'BIRD, ревизии, файловые логи и дополнительные ключи /v1/settings.',
|
||||
icon: <SlidersHorizontalIcon aria-hidden="true" />,
|
||||
badge: { label: 'operator', variant: 'warning-light' as const },
|
||||
badge: { label: 'оператор', variant: 'warning-light' as const },
|
||||
},
|
||||
{
|
||||
id: 'access',
|
||||
to: '/access' as const,
|
||||
search: undefined,
|
||||
title: 'Права доступа',
|
||||
description: 'API-ключи tenant, роли и управление доступом.',
|
||||
description: 'API-ключи арендатора, роли и управление доступом.',
|
||||
icon: <KeyRoundIcon aria-hidden="true" />,
|
||||
badge: { label: 'operator', variant: 'warning-light' as const },
|
||||
badge: { label: 'оператор', variant: 'warning-light' as const },
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
to: '/monitoring' as const,
|
||||
search: undefined,
|
||||
title: 'Мониторинг',
|
||||
description: 'Метрики, состояние jobs и observability control plane.',
|
||||
description: 'Метрики, состояние задач и наблюдаемость плоскости управления.',
|
||||
icon: <ActivityIcon aria-hidden="true" />,
|
||||
badge: { label: 'viewer+', variant: 'info-light' as const },
|
||||
badge: { label: 'просмотр+', variant: 'info-light' as const },
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -54,8 +54,8 @@ export function SectionsSettingsTab() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsCard
|
||||
title="Разделы control plane"
|
||||
description="Параметры tenant и операции — отдельно от настроек браузера"
|
||||
title="Разделы плоскости управления"
|
||||
description="Параметры арендатора и операции — отдельно от настроек браузера"
|
||||
>
|
||||
<ItemGroup className="gap-0">
|
||||
{ADMIN_SECTIONS.map((section, index) => (
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SessionSettingsTab() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsCard title="Текущая сессия" description="Проверка токена через GET /v1/auth/session">
|
||||
<SettingsCard title="Текущая сессия" description="Проверка токена через API сессии">
|
||||
<QueryState
|
||||
data={sessionQ.data}
|
||||
isLoading={sessionQ.isLoading && hasStoredToken}
|
||||
@@ -65,7 +65,7 @@ export function SessionSettingsTab() {
|
||||
</Badge>
|
||||
</ItemTitle>
|
||||
<ItemDescription className="leading-5">
|
||||
Tenant:{' '}
|
||||
Арендатор:{' '}
|
||||
<code className="text-foreground font-mono text-xs break-all">
|
||||
{session.tenant_id}
|
||||
</code>
|
||||
@@ -84,7 +84,7 @@ export function SessionSettingsTab() {
|
||||
>
|
||||
<SettingRow
|
||||
title="Роль"
|
||||
description="Определяет доступ к операциям control plane и CRUD."
|
||||
description="Определяет доступ к операциям плоскости управления и изменению данных."
|
||||
titleAddon={
|
||||
sessionQ.data ? (
|
||||
<Badge variant="info-light" size="sm">
|
||||
@@ -101,7 +101,7 @@ export function SessionSettingsTab() {
|
||||
>
|
||||
{sessionQ.data ? (
|
||||
<p className="text-muted-foreground text-sm break-words">
|
||||
Ключ с ролью <strong>{sessionQ.data.role}</strong> в tenant{' '}
|
||||
Ключ с ролью <strong>{ROLE_LABELS[sessionQ.data.role] ?? sessionQ.data.role}</strong> у арендатора{' '}
|
||||
<code className="font-mono text-xs break-all">{sessionQ.data.tenant_id}</code>.
|
||||
</p>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user