CI / changes (push) Successful in 14s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 59s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m47s
Added new mutations for creating, revoking, and rotating API keys in the api-keys query file. Updated the Access component to utilize these mutations, improving the user interface with better feedback and session management. Introduced a new AccessApiKeysCard for displaying API key information and enhanced the overall layout and user experience in the access route.
191 lines
7.3 KiB
TypeScript
191 lines
7.3 KiB
TypeScript
import { useState } from 'react'
|
|
import { Plus, RefreshCw, Trash2 } from 'lucide-react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@evobgp/ui/components/table'
|
|
|
|
import { ApiKeyCreateDialog } from '@/components/access/api-key-create-dialog'
|
|
import { ApiKeyTokenDialog } from '@/components/access/api-key-token-dialog'
|
|
import { Badge } from '@/components/reui/badge'
|
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
|
import { QueryState } from '@/components/query-state'
|
|
import { StatusBadge } from '@/components/status-badge'
|
|
import { TableSkeleton } from '@/components/skeletons'
|
|
import { formatApiKeyDate } from '@/lib/access/api-key-labels'
|
|
import { useRevokeApiKeyMutation, useRotateApiKeyMutation } from '@/queries/api-keys'
|
|
import type { ApiKey, ApiKeyCreated } from '@/types/api'
|
|
|
|
interface AccessApiKeysCardProps {
|
|
items: ApiKey[]
|
|
isLoading: boolean
|
|
isError: boolean
|
|
error: unknown
|
|
onRetry: () => void
|
|
}
|
|
|
|
export function AccessApiKeysCard({
|
|
items,
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
onRetry,
|
|
}: AccessApiKeysCardProps) {
|
|
const [createOpen, setCreateOpen] = useState(false)
|
|
const [tokenDialogOpen, setTokenDialogOpen] = useState(false)
|
|
const [revealedToken, setRevealedToken] = useState('')
|
|
|
|
const revoke = useRevokeApiKeyMutation()
|
|
const rotate = useRotateApiKeyMutation()
|
|
|
|
function showToken(created: ApiKeyCreated) {
|
|
setRevealedToken(created.token)
|
|
setTokenDialogOpen(true)
|
|
}
|
|
|
|
function handleRotated(id: string) {
|
|
rotate.mutate(id, {
|
|
onSuccess: (created) => showToken(created),
|
|
})
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader className="flex flex-col gap-3 border-b py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="min-w-0 flex-1">
|
|
<CardTitle className="text-base">API-ключи</CardTitle>
|
|
<CardDescription>
|
|
Управление ключами tenant. Полный токен показывается только при создании и ротации.
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
|
<Button size="sm" variant="outline" onClick={onRetry} disabled={isLoading}>
|
|
<RefreshCw className={isLoading ? 'animate-spin' : ''} />
|
|
Обновить
|
|
</Button>
|
|
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
|
<Plus />
|
|
Создать
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<QueryState
|
|
data={items}
|
|
isLoading={isLoading}
|
|
isError={isError}
|
|
error={error}
|
|
empty={items.length === 0}
|
|
emptyTitle="Нет ключей"
|
|
emptyDescription="Создайте API-ключ для автоматизации или отдельного доступа."
|
|
skeleton={<TableSkeleton rows={4} cols={6} />}
|
|
onRetry={onRetry}
|
|
>
|
|
{(data) => (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Имя</TableHead>
|
|
<TableHead>Роль</TableHead>
|
|
<TableHead>Префикс</TableHead>
|
|
<TableHead>Статус</TableHead>
|
|
<TableHead>Истекает</TableHead>
|
|
<TableHead>Последнее использование</TableHead>
|
|
<TableHead className="w-24" />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((k) => (
|
|
<TableRow key={k.id}>
|
|
<TableCell className="font-medium">{k.name}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="font-mono text-xs">
|
|
{k.role}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-xs text-muted-foreground">
|
|
{k.prefix}…
|
|
</TableCell>
|
|
<TableCell>
|
|
{k.revoked_at ? (
|
|
<StatusBadge status="error" label="отозван" />
|
|
) : (
|
|
<StatusBadge status="active" label="активен" />
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{formatApiKeyDate(k.expires_at)}
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{formatApiKeyDate(k.last_used_at)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex gap-1">
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
title="Ротировать"
|
|
disabled={!!k.revoked_at || rotate.isPending}
|
|
>
|
|
<RefreshCw className="size-3.5" />
|
|
</Button>
|
|
}
|
|
title="Ротировать ключ?"
|
|
description="Старый токен перестанет работать сразу."
|
|
confirmLabel="Ротировать"
|
|
onConfirm={() => handleRotated(k.id)}
|
|
/>
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className="text-destructive"
|
|
disabled={!!k.revoked_at || revoke.isPending}
|
|
title="Отозвать"
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</Button>
|
|
}
|
|
title="Отозвать API-ключ?"
|
|
description={`${k.name} (${k.prefix}…)`}
|
|
confirmLabel="Отозвать"
|
|
destructive
|
|
onConfirm={() => revoke.mutate(k.id)}
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</QueryState>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<ApiKeyCreateDialog
|
|
open={createOpen}
|
|
onOpenChange={setCreateOpen}
|
|
onCreated={showToken}
|
|
/>
|
|
|
|
<ApiKeyTokenDialog
|
|
open={tokenDialogOpen}
|
|
token={revealedToken}
|
|
onOpenChange={setTokenDialogOpen}
|
|
/>
|
|
</>
|
|
)
|
|
}
|