mirror of
https://github.com/langgenius/dify.git
synced 2026-07-27 23:18:33 +08:00
307 lines
11 KiB
TypeScript
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)}`
|
|
}
|