dify/web/features/agent-v2/agent-detail/access/components/agent-api-key-modal.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

307 lines
11 KiB
TypeScript

'use client'
import type { ApiKeyItem } from '@dify/contracts/api/console/agent/types.gen'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import {
Dialog,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback'
import useTimestamp from '@/hooks/use-timestamp'
import { consoleQuery } from '@/service/client'
export function AgentApiKeyModal({
agentId,
open,
onOpenChange,
}: {
agentId: string
open: boolean
onOpenChange: (open: boolean) => void
}) {
const { t } = useTranslation('appApi')
const { t: tCommon } = useTranslation('common')
const { formatTime } = useTimestamp()
const queryClient = useQueryClient()
const [newKey, setNewKey] = useState<ApiKeyItem | null>(null)
const [apiKeyToDelete, setApiKeyToDelete] = useState<ApiKeyItem | null>(null)
const apiKeysQueryOptions = consoleQuery.agent.byAgentId.apiKeys.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
},
})
const apiKeysQuery = useQuery({
...apiKeysQueryOptions,
enabled: open,
})
const createApiKeyMutation = useMutation(
consoleQuery.agent.byAgentId.apiKeys.post.mutationOptions({
onSuccess: (createdKey) => {
setNewKey(createdKey)
queryClient.invalidateQueries({ queryKey: apiKeysQueryOptions.queryKey })
queryClient.invalidateQueries({
queryKey: consoleQuery.agent.byAgentId.apiAccess.get.queryKey({
input: {
params: {
agent_id: agentId,
},
},
}),
})
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
},
onError: () => {
toast.error(tCommon(($) => $['actionMsg.modifiedUnsuccessfully']))
},
}),
)
const deleteApiKeyMutation = useMutation(
consoleQuery.agent.byAgentId.apiKeys.byApiKeyId.delete.mutationOptions({
onSuccess: () => {
setApiKeyToDelete(null)
queryClient.invalidateQueries({ queryKey: apiKeysQueryOptions.queryKey })
queryClient.invalidateQueries({
queryKey: consoleQuery.agent.byAgentId.apiAccess.get.queryKey({
input: {
params: {
agent_id: agentId,
},
},
}),
})
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
},
onError: () => {
toast.error(tCommon(($) => $['actionMsg.modifiedUnsuccessfully']))
},
}),
)
const apiKeys = apiKeysQuery.data?.data ?? []
const isCreating = createApiKeyMutation.isPending
const isDeleting = deleteApiKeyMutation.isPending
function handleCreateApiKey() {
createApiKeyMutation.mutate({
params: {
agent_id: agentId,
},
})
}
function handleDeleteApiKey() {
if (!apiKeyToDelete) return
deleteApiKeyMutation.mutate({
params: {
agent_id: agentId,
api_key_id: apiKeyToDelete.id,
},
})
}
function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) {
setNewKey(null)
setApiKeyToDelete(null)
}
onOpenChange(nextOpen)
}
return (
<>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="flex w-full max-w-[800px]! flex-col overflow-hidden px-8">
<DialogCloseButton />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(($) => $['apiKeyModal.apiSecretKey'])}
</DialogTitle>
<DialogDescription className="mt-1 system-sm-regular text-text-tertiary">
{t(($) => $['apiKeyModal.apiSecretKeyTips'])}
</DialogDescription>
<div className="mt-4 min-h-20 overflow-hidden">
<div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary">
<div className="w-64 shrink-0 px-3">{t(($) => $['apiKeyModal.secretKey'])}</div>
<div className="w-[200px] shrink-0 px-3">{t(($) => $['apiKeyModal.created'])}</div>
<div className="w-[200px] shrink-0 px-3">{t(($) => $['apiKeyModal.lastUsed'])}</div>
<div className="grow px-3" />
</div>
<div className="max-h-[280px] overflow-auto">
{apiKeysQuery.isPending && (
<div
role="status"
className="flex h-20 items-center justify-center system-sm-regular text-text-tertiary"
>
{t(($) => $.loading)}
</div>
)}
{apiKeysQuery.isError && (
<div className="flex h-20 items-center justify-center gap-2 system-sm-regular text-text-tertiary">
<span>{tCommon(($) => $['api.actionFailed'])}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
void apiKeysQuery.refetch()
}}
>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
)}
{apiKeysQuery.isSuccess && apiKeys.length === 0 && (
<div className="flex h-20 items-center justify-center system-sm-regular text-text-tertiary">
{tCommon(($) => $.noData)}
</div>
)}
{apiKeysQuery.isSuccess &&
apiKeys.map((apiKey) => (
<div
className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary last:border-b-0"
key={apiKey.id}
>
<div className="w-64 shrink-0 truncate px-3 font-mono" translate="no">
{maskApiKey(apiKey.token)}
</div>
<div className="w-[200px] shrink-0 truncate px-3">
{apiKey.created_at
? formatTime(
apiKey.created_at,
t(($) => $.dateTimeFormat, { ns: 'appLog' }),
)
: t(($) => $.never)}
</div>
<div className="w-[200px] shrink-0 truncate px-3">
{apiKey.last_used_at
? formatTime(
apiKey.last_used_at,
t(($) => $.dateTimeFormat, { ns: 'appLog' }),
)
: t(($) => $.never)}
</div>
<div className="flex grow gap-2 px-3">
<CopyFeedback content={apiKey.token} />
<Button
variant="ghost"
size="small"
className="size-6 px-0 text-text-tertiary hover:text-text-secondary"
aria-label={tCommon(($) => $['operation.delete'])}
disabled={isDeleting}
onClick={() => setApiKeyToDelete(apiKey)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</Button>
</div>
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-start">
<Button onClick={handleCreateApiKey} loading={isCreating}>
<span aria-hidden className="mr-1 i-heroicons-plus-20-solid size-4" />
{t(($) => $['apiKeyModal.createNewSecretKey'])}
</Button>
</div>
</DialogContent>
</Dialog>
<AgentApiKeyGenerateModal apiKey={newKey} onClose={() => setNewKey(null)} />
<AlertDialog
open={Boolean(apiKeyToDelete)}
onOpenChange={(nextOpen) => {
if (!nextOpen) setApiKeyToDelete(null)
}}
>
<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(($) => $['actionMsg.deleteConfirmTitle'])}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t(($) => $['actionMsg.deleteConfirmTips'])}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{tCommon(($) => $['operation.cancel'])}
</AlertDialogCancelButton>
<AlertDialogConfirmButton loading={isDeleting} onClick={handleDeleteApiKey}>
{tCommon(($) => $['operation.confirm'])}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</>
)
}
function AgentApiKeyGenerateModal({
apiKey,
onClose,
}: {
apiKey: ApiKeyItem | null
onClose: () => void
}) {
const { t } = useTranslation('appApi')
return (
<Dialog
open={Boolean(apiKey)}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose()
}}
>
<DialogContent className="w-full max-w-[480px]! overflow-hidden px-8">
<DialogCloseButton />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(($) => $['apiKeyModal.apiSecretKey'])}
</DialogTitle>
<DialogDescription className="mt-1 text-[13px] leading-5 font-normal text-text-tertiary">
{t(($) => $['apiKeyModal.generateTips'])}
</DialogDescription>
<div className="my-4 flex h-9 min-w-0 items-center rounded-lg bg-components-input-bg-normal px-2">
<span
className="min-w-0 flex-1 truncate font-mono system-sm-medium text-text-secondary"
translate="no"
>
{apiKey?.token}
</span>
{apiKey && <CopyFeedback content={apiKey.token} />}
</div>
<div className="my-4 flex justify-end">
<Button className="w-16 shrink-0" onClick={onClose}>
{t(($) => $['actionMsg.ok'])}
</Button>
</div>
</DialogContent>
</Dialog>
)
}
function maskApiKey(token: string) {
if (token.length <= 24) return token
return `${token.slice(0, 3)}...${token.slice(-20)}`
}