mirror of
https://github.com/langgenius/dify.git
synced 2026-07-26 22:28:32 +08:00
251 lines
7.8 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|