diff --git a/apps/web/src/components/access/access-api-keys-card.tsx b/apps/web/src/components/access/access-api-keys-card.tsx index 92d0c67..ba15238 100644 --- a/apps/web/src/components/access/access-api-keys-card.tsx +++ b/apps/web/src/components/access/access-api-keys-card.tsx @@ -49,7 +49,8 @@ export function AccessApiKeysCard({ <> @@ -83,8 +84,7 @@ export function ConnectionSettingsTab({ tokenRequired }: { tokenRequired: boolea title="Токен для запросов" description={ <> - Ключ для заголовка Authorization. Управление - ключами tenant — в разделе{' '} + Ключ для заголовка Authorization. Управление ключами tenant — в разделе{' '} Права доступа @@ -97,12 +97,14 @@ export function ConnectionSettingsTab({ tokenRequired }: { tokenRequired: boolea } labelFor="settings-token" + stacked last > setTokenValue(e.target.value)} placeholder="dev или API-ключ" diff --git a/apps/web/src/components/settings/sections-settings-tab.tsx b/apps/web/src/components/settings/sections-settings-tab.tsx index abf8024..62be1d9 100644 --- a/apps/web/src/components/settings/sections-settings-tab.tsx +++ b/apps/web/src/components/settings/sections-settings-tab.tsx @@ -49,7 +49,7 @@ const ADMIN_SECTIONS = [ export function SectionsSettingsTab() { return ( -
+
( {index > 0 ? : null} - + {section.icon} @@ -66,7 +66,7 @@ export function SectionsSettingsTab() { - + {section.title} {section.badge.label} diff --git a/apps/web/src/components/settings/session-settings-tab.tsx b/apps/web/src/components/settings/session-settings-tab.tsx index 5b10e60..762f269 100644 --- a/apps/web/src/components/settings/session-settings-tab.tsx +++ b/apps/web/src/components/settings/session-settings-tab.tsx @@ -35,7 +35,7 @@ export function SessionSettingsTab() { }) return ( -
+
- + Этот браузер Активна @@ -66,7 +66,9 @@ export function SessionSettingsTab() { Tenant:{' '} - {session.tenant_id} + + {session.tenant_id} + @@ -94,12 +96,13 @@ export function SessionSettingsTab() { ) } + stacked last > {sessionQ.data ? ( -

+

Ключ с ролью {sessionQ.data.role} в tenant{' '} - {sessionQ.data.tenant_id}. + {sessionQ.data.tenant_id}.

) : (

diff --git a/apps/web/src/components/settings/setting-row.tsx b/apps/web/src/components/settings/setting-row.tsx index fa79498..f0474b0 100644 --- a/apps/web/src/components/settings/setting-row.tsx +++ b/apps/web/src/components/settings/setting-row.tsx @@ -10,31 +10,59 @@ import { FieldTitle, } from '@evobgp/ui/components/field' -interface SettingRowProps { +export interface SettingRowProps { title: string description?: ReactNode children: ReactNode last?: boolean + /** Opt-in FieldSeparator after the row (default off — ReUI settings use Frame+gap). */ + separated?: boolean + compact?: boolean + stacked?: boolean labelFor?: string contentClassName?: string + className?: string titleAddon?: ReactNode } -/** Settings row — settings-3 DNA. Preview: https://reui.io/preview/base/settings-3 */ +/** + * Settings row — settings-3 / settings-16 DNA. + * Control is flex-1 + min-w-0 (never a fixed w-78) so label and input do not overlap. + * @see https://reui.io/preview/base/settings-3 + * @see https://reui.io/preview/base/settings-16 + */ export function SettingRow({ title, description, children, last, + separated = false, + compact, + stacked, labelFor, contentClassName, + className, titleAddon, }: SettingRowProps) { return ( <> - -

-
+ +
+
{labelFor ? ( {title} ) : ( @@ -44,20 +72,33 @@ export function SettingRow({
{description ? ( - {description} + {description} ) : null}
-
+
{children}
- {!last ? : null} + {separated && !last ? : null} ) } diff --git a/apps/web/src/components/settings/settings-card.tsx b/apps/web/src/components/settings/settings-card.tsx index 6b6139b..1936034 100644 --- a/apps/web/src/components/settings/settings-card.tsx +++ b/apps/web/src/components/settings/settings-card.tsx @@ -31,7 +31,7 @@ export function SettingsCard({ footerClassName, }: SettingsCardProps) { return ( - + {title} diff --git a/apps/web/src/components/settings/settings-setting-field.tsx b/apps/web/src/components/settings/settings-setting-field.tsx index 8dac7f1..a668f12 100644 --- a/apps/web/src/components/settings/settings-setting-field.tsx +++ b/apps/web/src/components/settings/settings-setting-field.tsx @@ -1,17 +1,9 @@ import { type ComponentProps, type ReactNode } from 'react' import { Badge } from '@/components/reui/badge' -import { cn } from '@evobgp/ui/lib/utils' -import { - Field, - FieldContent, - FieldDescription, - FieldLabel, - FieldSeparator, - FieldTitle, -} from '@evobgp/ui/components/field' +import { SettingRow } from '@/components/settings/setting-row' -/** settings-3 row pattern for tenant/UI settings. */ +/** Tenant settings row — same SettingRow DNA as UI settings. */ export function SettingsSettingField({ title, description, @@ -20,38 +12,34 @@ export function SettingsSettingField({ last, labelFor, contentClassName, + stacked, }: { title: string - description: ReactNode + description?: ReactNode badge?: { label: string; variant: ComponentProps['variant'] } children: ReactNode last?: boolean labelFor?: string contentClassName?: string + stacked?: boolean }) { return ( - <> - -
-
- {labelFor ? ( - {title} - ) : ( - {title} - )} - {badge ? ( - - {badge.label} - - ) : null} -
- {description} -
- - {children} - -
- {!last ? : null} - + + {badge.label} + + ) : null + } + > + {children} + ) } diff --git a/apps/web/src/routes/_auth/_settings/tenant-settings.tsx b/apps/web/src/routes/_auth/_settings/tenant-settings.tsx index 03c73f3..c120645 100644 --- a/apps/web/src/routes/_auth/_settings/tenant-settings.tsx +++ b/apps/web/src/routes/_auth/_settings/tenant-settings.tsx @@ -4,12 +4,12 @@ import { Save } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' -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' import { FrameDataGrid } from '@/components/reui-kit' -import { SelectField } from '@/components/select-field' +import { SelectMenu } from '@/components/select-field' +import { SettingsCard } from '@/components/settings/settings-card' +import { SettingsFieldGroup } from '@/components/settings/settings-field-group' import { SettingsKvGrid } from '@/components/settings/settings-kv-grid' import { SettingsSettingField } from '@/components/settings/settings-setting-field' import { QueryState } from '@/components/query-state' @@ -114,7 +114,7 @@ function TenantSettingsComponent() { } return ( -
+
@@ -130,90 +130,104 @@ function TenantSettingsComponent() { ]} > - - Глобальные параметры BIRD для pipeline refresh/apply. Сохранение через{' '} - PATCH /v1/settings (роль operator). - + description="Глобальные параметры BIRD для pipeline refresh/apply. Сохранение — роль operator." + footer={ + + + Сохранить + } - contentClassName="space-y-4 py-4" > - } - onRetry={() => settingsQ.refetch()} - > - {() => ( -
- {BIRD_SETTING_KEYS.map((key, index) => ( - - {key === 'peer_discovery_enabled' || - key === 'peer_discovery_require_external' ? ( - - setBirdForm((s) => ({ ...s, [key]: v || 'false' })) - } - placeholder="Выкл" - /> - ) : ( - - setBirdForm((s) => ({ ...s, [key]: e.target.value })) - } - placeholder={BIRD_LABELS[key]} - /> - )} - - ))} -
- - - Сохранить - -
-
- )} -
-
+ } + onRetry={() => settingsQ.refetch()} + > + {() => ( + + {BIRD_SETTING_KEYS.map((key, index) => ( + + {key === 'peer_discovery_enabled' || + key === 'peer_discovery_require_external' ? ( + + setBirdForm((s) => ({ ...s, [key]: v || 'false' })) + } + placeholder="Выкл" + /> + ) : ( + + setBirdForm((s) => ({ ...s, [key]: e.target.value })) + } + placeholder={BIRD_LABELS[key]} + /> + )} + + ))} + + )} + +
- + + Сохранить + + } > - } - onRetry={() => settingsQ.refetch()} - > - {() => ( -
- + } + onRetry={() => settingsQ.refetch()} + > + {() => ( + + setRevisionForm((s) => ({ @@ -222,45 +236,46 @@ function TenantSettingsComponent() { })) } /> -

- revision_retention_minutes -

- - - Сохранить - -
- )} -
-
+ + + )} + +
- + + Сохранить + + } > - } - onRetry={() => settingsQ.refetch()} - > - {() => ( -
- } + onRetry={() => settingsQ.refetch()} + > + {() => ( + + + v && setRuntimeLogsForm((s) => ({ @@ -269,45 +284,54 @@ function TenantSettingsComponent() { })) } /> -
- - - setRuntimeLogsForm((s) => ({ - ...s, - runtime_logs_max_file_mb: e.target.value, - })) - } - /> -

- runtime_logs_max_file_mb -

-
-
- - - setRuntimeLogsForm((s) => ({ - ...s, - runtime_logs_auto_schedule: e.target.value, - })) - } - /> -

- runtime_logs_auto_schedule -

-
- + + + setRuntimeLogsForm((s) => ({ + ...s, + runtime_logs_max_file_mb: e.target.value, + })) + } + /> + + + + setRuntimeLogsForm((s) => ({ + ...s, + runtime_logs_auto_schedule: e.target.value, + })) + } + /> + + + v && setRuntimeLogsForm((s) => ({ @@ -316,16 +340,11 @@ function TenantSettingsComponent() { })) } /> -
- - - Сохранить - -
-
- )} -
-
+ + + )} + +
diff --git a/apps/web/src/routes/_auth/access.tsx b/apps/web/src/routes/_auth/access.tsx index 1204d87..546a9b5 100644 --- a/apps/web/src/routes/_auth/access.tsx +++ b/apps/web/src/routes/_auth/access.tsx @@ -1,15 +1,23 @@ import { createFileRoute, Link } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react' +import { KeyRound, MonitorIcon, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { PanelCard } from '@/components/panel-card' +import { + Item, + ItemContent, + ItemDescription, + ItemGroup, + ItemMedia, + ItemTitle, +} from '@evobgp/ui/components/item' import { AccessApiKeysCard } from '@/components/access/access-api-keys-card' import { PageHeader } from '@/components/page-header' import { Badge } from '@/components/reui/badge' import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit' +import { SettingsCard } from '@/components/settings/settings-card' import { SectionCardsSkeleton } from '@/components/skeletons' import { sessionCanManageApiKeys } from '@/lib/auth' import { authSessionQueryOptions } from '@/queries/auth' @@ -38,6 +46,7 @@ function AccessComponent() { const kpiItems: KpiStatItem[] = useMemo( () => [ { + id: 'keys-total', label: 'Всего ключей', value: keys.length, icon: , @@ -48,6 +57,7 @@ function AccessComponent() { ), }, { + id: 'keys-active', label: 'Активных', value: activeCount, icon: , @@ -58,6 +68,7 @@ function AccessComponent() { ), }, { + id: 'keys-revoked', label: 'Отозванных', value: revokedCount, icon: , @@ -93,7 +104,7 @@ function AccessComponent() { })() return ( -
+
{session ? ( - -
-

Tenant

-

{session.tenant_id}

-
-
-

Доступ

-

{sessionAccessLabel}

-
- {sessionKindLabel ? ( -
-

Тип

-

{sessionKindLabel}

-
- ) : null} - {session.email ? ( -
-

Email

-

{session.email}

-
- ) : null} -
+ + + + + + + +
+ Этот браузер + {sessionKindLabel ? ( + + {sessionKindLabel} + + ) : null} +
+ + + Tenant:{' '} + {session.tenant_id} + + Доступ: {sessionAccessLabel} + {session.email ? Email: {session.email} : null} + +
+
+
+ ) : ( - - Не удалось определить сессию. Укажите токен в{' '} - - настройках - {' '} - (для dev-окружения — dev при включённом demo-seed). - {sessionQuery.isError && sessionQuery.error instanceof Error ? ( - {sessionQuery.error.message} - ) : null} - + +

+ Не удалось определить сессию. Укажите токен в{' '} + + настройках + {' '} + (для dev-окружения — dev при включённом demo-seed). + {sessionQuery.isError && sessionQuery.error instanceof Error ? ( + {sessionQuery.error.message} + ) : null} +

+
)} {canManageKeys ? ( @@ -167,11 +186,13 @@ function AccessComponent() { /> ) : session ? ( - - Управление API-ключами доступно роли operator (API-ключ) или portal JWT - с is_admin / правом bgp:access:admin. - Текущий доступ: {sessionAccessLabel}. - + +

+ Управление API-ключами доступно роли operator (API-ключ) или portal JWT + с is_admin / правом bgp:access:admin. + Текущий доступ: {sessionAccessLabel}. +

+
) : null}
)