CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m6s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m27s
Updated multiple components to improve user interface consistency by replacing traditional badge implementations with the new CategoryBadge and DataGridPrimaryCell components. Enhanced the StatusBadge component to support additional status variants and integrated it across various grids, including AccessApiKeysGrid, DashboardRecentJobsGrid, and OperationsJobsGrid. This refactor streamlines the presentation of data and improves the overall user experience across the application.
158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import { ColumnDef } from '@tanstack/react-table'
|
|
import { RefreshCw, Trash2 } from 'lucide-react'
|
|
import { useMemo } from 'react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
|
|
import { CategoryBadge } from '@/components/category-badge'
|
|
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
|
|
import { DataGridSection } from '@/components/data-grid-shell'
|
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
|
import { StatusBadge } from '@/components/status-badge'
|
|
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
|
import { formatApiKeyDate } from '@/lib/access/api-key-labels'
|
|
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
|
|
import type { ApiKey } from '@/types/api'
|
|
|
|
export function AccessApiKeysGrid({
|
|
items,
|
|
isLoading = false,
|
|
onRotate,
|
|
onRevoke,
|
|
rotatePending = false,
|
|
revokePending = false,
|
|
}: {
|
|
items: ApiKey[]
|
|
isLoading?: boolean
|
|
onRotate: (id: string) => void
|
|
onRevoke: (id: string) => void
|
|
rotatePending?: boolean
|
|
revokePending?: boolean
|
|
}) {
|
|
const columns = useMemo<ColumnDef<ApiKey>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: 'name',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Имя" />,
|
|
cell: ({ row }) => <DataGridPrimaryCell title={row.original.name} accent="primary" />,
|
|
meta: { headerTitle: 'Имя' },
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Роль" />,
|
|
cell: ({ row }) => (
|
|
<CategoryBadge className="font-mono text-xs">{row.original.role}</CategoryBadge>
|
|
),
|
|
meta: { headerTitle: 'Роль' },
|
|
},
|
|
{
|
|
accessorKey: 'prefix',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Префикс" />,
|
|
cell: ({ row }) => (
|
|
<span className="font-mono text-xs text-muted-foreground">{row.original.prefix}…</span>
|
|
),
|
|
meta: { headerTitle: 'Префикс' },
|
|
},
|
|
{
|
|
id: 'status',
|
|
enableSorting: false,
|
|
header: 'Статус',
|
|
cell: ({ row }) =>
|
|
row.original.revoked_at ? (
|
|
<StatusBadge status="error" label="отозван" />
|
|
) : (
|
|
<StatusBadge status="active" label="активен" />
|
|
),
|
|
meta: { headerTitle: 'Статус' },
|
|
},
|
|
{
|
|
id: 'expires_at',
|
|
accessorFn: (row) => row.expires_at ?? '',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Истекает" />,
|
|
cell: ({ row }) => (
|
|
<DataGridMutedCell>{formatApiKeyDate(row.original.expires_at)}</DataGridMutedCell>
|
|
),
|
|
meta: { headerTitle: 'Истекает' },
|
|
},
|
|
{
|
|
id: 'last_used_at',
|
|
accessorFn: (row) => row.last_used_at ?? '',
|
|
header: ({ column }) => <DataGridColumnHeader column={column} title="Последнее использование" />,
|
|
cell: ({ row }) => (
|
|
<DataGridMutedCell>{formatApiKeyDate(row.original.last_used_at)}</DataGridMutedCell>
|
|
),
|
|
meta: { headerTitle: 'Последнее использование' },
|
|
},
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => null,
|
|
cell: ({ row }) => {
|
|
const k = row.original
|
|
return (
|
|
<div className="flex gap-1">
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
type="button"
|
|
title="Ротировать"
|
|
disabled={!!k.revoked_at || rotatePending}
|
|
>
|
|
<RefreshCw className="size-3.5" />
|
|
</Button>
|
|
}
|
|
title="Ротировать ключ?"
|
|
description="Старый токен перестанет работать сразу."
|
|
confirmLabel="Ротировать"
|
|
onConfirm={() => onRotate(k.id)}
|
|
/>
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
type="button"
|
|
className="text-destructive"
|
|
disabled={!!k.revoked_at || revokePending}
|
|
title="Отозвать"
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</Button>
|
|
}
|
|
title="Отозвать API-ключ?"
|
|
description={`${k.name} (${k.prefix}…)`}
|
|
confirmLabel="Отозвать"
|
|
destructive
|
|
onConfirm={() => onRevoke(k.id)}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
],
|
|
[onRevoke, onRotate, revokePending, rotatePending],
|
|
)
|
|
|
|
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
|
|
data: items,
|
|
columns,
|
|
getSearchText: (row) =>
|
|
`${row.name} ${row.role} ${row.prefix} ${row.revoked_at ? 'отозван' : 'активен'}`,
|
|
getRowId: (row) => row.id,
|
|
})
|
|
|
|
return (
|
|
<DataGridSection
|
|
table={table}
|
|
recordCount={filteredCount}
|
|
isLoading={isLoading}
|
|
emptyMessage="Нет ключей"
|
|
searchValue={globalFilter}
|
|
onSearchChange={setGlobalFilter}
|
|
searchPlaceholder="Поиск API-ключей…"
|
|
/>
|
|
)
|
|
}
|