Enhanced various settings components by adding 'min-w-0' class for better overflow handling and adjusting flex properties for improved alignment. Updated card and grid structures to ensure a cohesive user experience across the settings interface. Refined descriptions and layout in several settings tabs for better clarity and usability.
200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
import { createFileRoute, Link } from '@tanstack/react-router'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { KeyRound, MonitorIcon, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react'
|
||
import { useMemo } from 'react'
|
||
|
||
import { Button } from '@evobgp/ui/components/button'
|
||
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'
|
||
import { apiKeysQueryOptions } from '@/queries/api-keys'
|
||
|
||
export const Route = createFileRoute('/_auth/access')({
|
||
component: AccessComponent,
|
||
})
|
||
|
||
function AccessComponent() {
|
||
const sessionQuery = useQuery(authSessionQueryOptions())
|
||
const session = sessionQuery.data ?? null
|
||
const canManageKeys = sessionCanManageApiKeys(session)
|
||
|
||
const keysQuery = useQuery({
|
||
...apiKeysQueryOptions(),
|
||
enabled: canManageKeys,
|
||
})
|
||
|
||
const keys = keysQuery.data ?? []
|
||
const activeCount = keys.filter((k) => !k.revoked_at).length
|
||
const revokedCount = keys.filter((k) => k.revoked_at).length
|
||
|
||
const refreshing = sessionQuery.isFetching || keysQuery.isFetching
|
||
|
||
const kpiItems: KpiStatItem[] = useMemo(
|
||
() => [
|
||
{
|
||
id: 'keys-total',
|
||
label: 'Всего ключей',
|
||
value: keys.length,
|
||
icon: <KeyRound className="size-4" />,
|
||
footer: (
|
||
<Badge variant="primary-light" size="sm">
|
||
в tenant
|
||
</Badge>
|
||
),
|
||
},
|
||
{
|
||
id: 'keys-active',
|
||
label: 'Активных',
|
||
value: activeCount,
|
||
icon: <ShieldCheck className="size-4" />,
|
||
footer: (
|
||
<Badge variant="success-light" size="sm">
|
||
не отозваны
|
||
</Badge>
|
||
),
|
||
},
|
||
{
|
||
id: 'keys-revoked',
|
||
label: 'Отозванных',
|
||
value: revokedCount,
|
||
icon: <ShieldOff className="size-4" />,
|
||
variant: revokedCount > 0 ? 'warning' : 'default',
|
||
footer: (
|
||
<Badge variant={revokedCount > 0 ? 'warning-light' : 'outline'} size="sm">
|
||
отозваны
|
||
</Badge>
|
||
),
|
||
},
|
||
],
|
||
[keys.length, activeCount, revokedCount],
|
||
)
|
||
|
||
function refetchAll() {
|
||
void sessionQuery.refetch()
|
||
if (canManageKeys) void keysQuery.refetch()
|
||
}
|
||
|
||
const sessionKindLabel =
|
||
session?.kind === 'jwt' ? 'Portal JWT' : session?.kind === 'apikey' ? 'API-ключ' : null
|
||
|
||
const sessionAccessLabel = (() => {
|
||
if (!session) return null
|
||
if (session.kind === 'jwt' || session.is_admin || (session.permissions?.length ?? 0) > 0) {
|
||
if (session.is_admin) return 'admin (portal)'
|
||
if (sessionCanManageApiKeys(session)) return 'bgp:access:admin'
|
||
return session.permissions?.length
|
||
? session.permissions.slice(0, 3).join(', ')
|
||
: 'без access:admin'
|
||
}
|
||
return session.role || '—'
|
||
})()
|
||
|
||
return (
|
||
<div className="flex min-w-0 flex-col gap-6">
|
||
<PageHeader
|
||
title="Права доступа"
|
||
description="API-ключи control plane и текущая сессия Bearer-токена."
|
||
actions={
|
||
canManageKeys ? (
|
||
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
|
||
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
|
||
Обновить
|
||
</Button>
|
||
) : undefined
|
||
}
|
||
/>
|
||
|
||
{session ? (
|
||
<SettingsCard
|
||
title="Текущая сессия"
|
||
description="Tenant и права текущего Bearer (API-ключ или portal JWT)."
|
||
>
|
||
<ItemGroup className="gap-0">
|
||
<Item className="min-h-0 min-w-0 items-start gap-4 px-5 py-3.5">
|
||
<ItemMedia className="self-start">
|
||
<Item className="bg-muted/60 border-background flex size-8 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] [&_svg]:size-4 [&_svg]:opacity-60">
|
||
<MonitorIcon aria-hidden="true" />
|
||
</Item>
|
||
</ItemMedia>
|
||
<ItemContent className="min-w-0 gap-3">
|
||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||
<ItemTitle className="leading-5">Этот браузер</ItemTitle>
|
||
{sessionKindLabel ? (
|
||
<Badge variant="info-light" size="sm">
|
||
{sessionKindLabel}
|
||
</Badge>
|
||
) : null}
|
||
</div>
|
||
<ItemDescription className="flex min-w-0 flex-col gap-1 leading-5">
|
||
<span className="break-all">
|
||
Tenant:{' '}
|
||
<code className="text-foreground font-mono text-xs">{session.tenant_id}</code>
|
||
</span>
|
||
<span className="break-words">Доступ: {sessionAccessLabel}</span>
|
||
{session.email ? <span className="break-all">Email: {session.email}</span> : null}
|
||
</ItemDescription>
|
||
</ItemContent>
|
||
</Item>
|
||
</ItemGroup>
|
||
</SettingsCard>
|
||
) : (
|
||
<SettingsCard title="Текущая сессия">
|
||
<p className="text-muted-foreground px-5 py-4 text-sm">
|
||
Не удалось определить сессию. Укажите токен в{' '}
|
||
<Link
|
||
to="/settings"
|
||
search={{ tab: 'connection' }}
|
||
className="text-primary underline-offset-4 hover:underline"
|
||
>
|
||
настройках
|
||
</Link>{' '}
|
||
(для dev-окружения — <code className="text-xs">dev</code> при включённом demo-seed).
|
||
{sessionQuery.isError && sessionQuery.error instanceof Error ? (
|
||
<span className="text-destructive mt-2 block">{sessionQuery.error.message}</span>
|
||
) : null}
|
||
</p>
|
||
</SettingsCard>
|
||
)}
|
||
|
||
{canManageKeys ? (
|
||
<>
|
||
{keysQuery.isLoading ? (
|
||
<SectionCardsSkeleton count={3} />
|
||
) : (
|
||
<KpiStatGrid items={kpiItems} />
|
||
)}
|
||
<AccessApiKeysCard
|
||
items={keys}
|
||
isLoading={keysQuery.isLoading}
|
||
isError={keysQuery.isError}
|
||
error={keysQuery.error}
|
||
onRetry={() => keysQuery.refetch()}
|
||
/>
|
||
</>
|
||
) : session ? (
|
||
<SettingsCard title="API-ключи">
|
||
<p className="text-muted-foreground px-5 py-4 text-sm break-words">
|
||
Управление API-ключами доступно роли <strong>operator</strong> (API-ключ) или portal JWT
|
||
с <strong>is_admin</strong> / правом <code className="text-xs">bgp:access:admin</code>.
|
||
Текущий доступ: <span className="font-mono">{sessionAccessLabel}</span>.
|
||
</p>
|
||
</SettingsCard>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|