dify/web/features/deployments/detail/api-tokens/api-keys/api-key-list.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

251 lines
7.8 KiB
TypeScript

'use client'
import type { ApiKey, Environment } from '@dify/contracts/enterprise/types.gen'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import {
DetailTable,
DetailTableBody,
DetailTableCard,
DetailTableCardList,
DetailTableCell,
DetailTableHead,
DetailTableHeader,
DetailTableRow,
} from '../../../shared/components/detail-table'
import { API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES } from '../table-styles'
function ApiKeyName({ apiKey }: { apiKey: ApiKey }) {
return <span className="block truncate text-text-primary">{apiKey.displayName}</span>
}
function EnvironmentBadge({ environment }: { environment?: Environment }) {
return (
<span className="inline-flex h-5 max-w-36 items-center rounded-md bg-background-section-burn px-1.5 text-xs text-text-tertiary">
<span className="truncate">{environment?.displayName ?? '—'}</span>
</span>
)
}
function ApiKeyValue({ value }: { value: string }) {
return (
<div className="flex h-8 min-w-0 items-center rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-2">
<div className="min-w-0 flex-1 truncate font-mono system-sm-medium text-text-secondary">
{value}
</div>
</div>
)
}
function RevokeApiKeyButton({ apiKey }: { apiKey: ApiKey }) {
const { t } = useTranslation('deployments')
const [showRevokeConfirm, setShowRevokeConfirm] = useState(false)
const revokeApiKey = useMutation(
consoleQuery.enterprise.accessService.deleteApiKey.mutationOptions(),
)
const isRevoking = revokeApiKey.isPending
const apiKeyName = apiKey.displayName
function handleRevoke() {
if (isRevoking) return
revokeApiKey.mutate(
{
params: {
appInstanceId: apiKey.appInstanceId,
environmentId: apiKey.environmentId,
apiKeyId: apiKey.id,
},
},
{
onSuccess: () => {
setShowRevokeConfirm(false)
toast.success(t(($) => $['access.api.revokeSuccess']))
},
onError: () => {
toast.error(t(($) => $['access.api.revokeFailed']))
},
},
)
}
function handleRevokeConfirmOpenChange(open: boolean) {
if (isRevoking) return
setShowRevokeConfirm(open)
}
return (
<>
<button
type="button"
onClick={() => setShowRevokeConfirm(true)}
aria-label={t(($) => $['access.revoke'])}
aria-busy={isRevoking}
disabled={isRevoking}
className={cn(
'inline-flex size-8 shrink-0 items-center justify-center rounded-md text-text-tertiary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
isRevoking
? 'cursor-not-allowed opacity-60'
: 'hover:bg-state-destructive-hover hover:text-text-destructive',
)}
>
<span
className={cn(
isRevoking ? 'i-ri-loader-2-line animate-spin' : 'i-ri-delete-bin-line',
'size-3.5',
)}
/>
</button>
<AlertDialog open={showRevokeConfirm} onOpenChange={handleRevokeConfirmOpenChange}>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
{t(($) => $['access.api.revokeConfirmTitle'])}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t(($) => $['access.api.revokeConfirmDescription'], { name: apiKeyName })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton disabled={isRevoking}>
{t(($) => $['operation.cancel'], { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
loading={isRevoking}
disabled={isRevoking}
onClick={handleRevoke}
>
{t(($) => $['access.revoke'])}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</>
)
}
function ApiKeyMobileRow({ apiKey, environment }: { apiKey: ApiKey; environment?: Environment }) {
const { t } = useTranslation('deployments')
const displayValue = apiKey.maskedToken
return (
<DetailTableCard>
<div className="flex flex-col gap-3 p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<ApiKeyName apiKey={apiKey} />
<div className="mt-1">
<EnvironmentBadge environment={environment} />
</div>
</div>
<RevokeApiKeyButton apiKey={apiKey} />
</div>
<div className="flex min-w-0 flex-col gap-1">
<span className="system-2xs-medium-uppercase text-text-tertiary">
{t(($) => $['access.api.table.key'])}
</span>
<ApiKeyValue value={displayValue} />
</div>
</div>
</DetailTableCard>
)
}
function ApiKeyDesktopRow({ apiKey, environment }: { apiKey: ApiKey; environment?: Environment }) {
const displayValue = apiKey.maskedToken
return (
<DetailTableRow>
<DetailTableCell>
<ApiKeyName apiKey={apiKey} />
</DetailTableCell>
<DetailTableCell>
<EnvironmentBadge environment={environment} />
</DetailTableCell>
<DetailTableCell>
<ApiKeyValue value={displayValue} />
</DetailTableCell>
<DetailTableCell>
<div className="flex justify-end">
<RevokeApiKeyButton apiKey={apiKey} />
</div>
</DetailTableCell>
</DetailTableRow>
)
}
function ApiKeyTableHeader() {
const { t } = useTranslation('deployments')
return (
<DetailTableHeader>
<DetailTableRow>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.name}>
{t(($) => $['access.api.table.name'])}
</DetailTableHead>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.environment}>
{t(($) => $['access.api.table.environment'])}
</DetailTableHead>
<DetailTableHead className={API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.key}>
{t(($) => $['access.api.table.key'])}
</DetailTableHead>
<DetailTableHead className={`${API_KEY_DETAIL_TABLE_COLUMN_CLASS_NAMES.action} text-right`}>
{t(($) => $['access.api.table.action'])}
</DetailTableHead>
</DetailTableRow>
</DetailTableHeader>
)
}
export function ApiKeyList({
apiKeys,
environments,
}: {
apiKeys: ApiKey[]
environments: Environment[]
}) {
const environmentById = new Map(environments.map((environment) => [environment.id, environment]))
return (
<>
<DetailTableCardList className={cn('pc:hidden')}>
{apiKeys.map((apiKey) => (
<ApiKeyMobileRow
key={apiKey.id}
apiKey={apiKey}
environment={environmentById.get(apiKey.environmentId)}
/>
))}
</DetailTableCardList>
<div className="hidden pc:block">
<DetailTable>
<ApiKeyTableHeader />
<DetailTableBody>
{apiKeys.map((apiKey) => (
<ApiKeyDesktopRow
key={apiKey.id}
apiKey={apiKey}
environment={environmentById.get(apiKey.environmentId)}
/>
))}
</DetailTableBody>
</DetailTable>
</div>
</>
)
}