mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 17:32:13 +08:00
217 lines
8.6 KiB
TypeScript
217 lines
8.6 KiB
TypeScript
'use client'
|
|
import type { CreateApiKeyResponse } from '@/models/app'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogActions,
|
|
AlertDialogCancelButton,
|
|
AlertDialogConfirmButton,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogTitle,
|
|
} from '@langgenius/dify-ui/alert-dialog'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
|
import { useAtomValue } from 'jotai'
|
|
import {
|
|
useState,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import ActionButton from '@/app/components/base/action-button'
|
|
import CopyFeedback from '@/app/components/base/copy-feedback'
|
|
import Loading from '@/app/components/base/loading'
|
|
import { currentWorkspaceAtom } from '@/context/app-context-state'
|
|
import useTimestamp from '@/hooks/use-timestamp'
|
|
import {
|
|
createApikey as createAppApikey,
|
|
delApikey as delAppApikey,
|
|
} from '@/service/apps'
|
|
import {
|
|
createApikey as createDatasetApikey,
|
|
delApikey as delDatasetApikey,
|
|
} from '@/service/datasets'
|
|
import { useDatasetApiKeys, useInvalidateDatasetApiKeys } from '@/service/knowledge/use-dataset'
|
|
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
|
|
import SecretKeyGenerateModal from './secret-key-generate'
|
|
import s from './style.module.css'
|
|
|
|
type ISecretKeyModalProps = {
|
|
isShow: boolean
|
|
appId?: string
|
|
canManage: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
const SecretKeyModal = ({
|
|
isShow = false,
|
|
appId,
|
|
canManage,
|
|
onClose,
|
|
}: ISecretKeyModalProps) => {
|
|
const { t } = useTranslation()
|
|
const { formatTime } = useTimestamp()
|
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
|
|
const invalidateAppApiKeys = useInvalidateAppApiKeys()
|
|
const invalidateDatasetApiKeys = useInvalidateDatasetApiKeys()
|
|
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
|
|
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading } = useDatasetApiKeys({ enabled: !appId && isShow })
|
|
const apiKeysList = appId ? appApiKeys : datasetApiKeys
|
|
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
|
|
|
|
const [delKeyID, setDelKeyId] = useState('')
|
|
|
|
const onDel = async () => {
|
|
setShowConfirmDelete(false)
|
|
if (!canManage)
|
|
return
|
|
if (!delKeyID)
|
|
return
|
|
|
|
const delApikey = appId ? delAppApikey : delDatasetApikey
|
|
const params = appId
|
|
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
|
|
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
|
|
await delApikey(params)
|
|
if (appId)
|
|
invalidateAppApiKeys(appId)
|
|
else
|
|
invalidateDatasetApiKeys()
|
|
}
|
|
|
|
const onCreate = async () => {
|
|
if (!currentWorkspace.id || !canManage)
|
|
return
|
|
|
|
const params = appId
|
|
? { url: `/apps/${appId}/api-keys`, body: {} }
|
|
: { url: '/datasets/api-keys', body: {} }
|
|
const createApikey = appId ? createAppApikey : createDatasetApikey
|
|
const res = await createApikey(params)
|
|
setIsVisible(true)
|
|
setNewKey(res)
|
|
if (appId)
|
|
invalidateAppApiKeys(appId)
|
|
else
|
|
invalidateDatasetApiKeys()
|
|
}
|
|
|
|
const generateToken = (token: string) => {
|
|
return `${token.slice(0, 3)}...${token.slice(-20)}`
|
|
}
|
|
|
|
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
|
if (open)
|
|
return
|
|
|
|
setDelKeyId('')
|
|
setShowConfirmDelete(false)
|
|
}
|
|
|
|
const handleClose = () => {
|
|
setIsVisible(false)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Dialog
|
|
open={isShow}
|
|
onOpenChange={(open) => {
|
|
if (!open)
|
|
handleClose()
|
|
}}
|
|
>
|
|
<DialogContent className={cn('max-h-[calc(100vh-80px)]! w-full max-w-[800px]! overflow-hidden! border-none text-left align-middle', `${s.customModal} flex flex-col px-8`)}>
|
|
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
|
{`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
|
|
</DialogTitle>
|
|
|
|
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
|
|
<button
|
|
type="button"
|
|
aria-label={t('operation.close', { ns: 'common' })}
|
|
className="flex size-6 cursor-pointer items-center justify-center text-text-tertiary"
|
|
onClick={handleClose}
|
|
>
|
|
<span className="i-heroicons-x-mark-20-solid size-6 cursor-pointer" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
|
|
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
|
|
{
|
|
!!apiKeysList?.data?.length && (
|
|
<div className="mt-4 flex grow flex-col 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', { ns: 'appApi' })}</div>
|
|
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.created', { ns: 'appApi' })}</div>
|
|
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.lastUsed', { ns: 'appApi' })}</div>
|
|
<div className="grow px-3"></div>
|
|
</div>
|
|
<div className="grow overflow-auto">
|
|
{apiKeysList.data.map(api => (
|
|
<div className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary" key={api.id}>
|
|
<div className="w-64 shrink-0 truncate px-3 font-mono">{generateToken(api.token)}</div>
|
|
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
|
|
<div className="w-[200px] shrink-0 truncate px-3">{api.last_used_at ? formatTime(Number(api.last_used_at), t('dateTimeFormat', { ns: 'appLog' }) as string) : t('never', { ns: 'appApi' })}</div>
|
|
<div className="flex grow space-x-2 px-3">
|
|
<CopyFeedback content={api.token} />
|
|
{canManage && (
|
|
<ActionButton
|
|
onClick={() => {
|
|
setDelKeyId(api.id)
|
|
setShowConfirmDelete(true)
|
|
}}
|
|
>
|
|
<span className="i-ri-delete-bin-line size-4" />
|
|
</ActionButton>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
<div className="flex">
|
|
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace.id || !canManage}>
|
|
<span className="mr-1 i-heroicons-plus-20-solid flex size-4 shrink-0" />
|
|
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
|
|
</Button>
|
|
</div>
|
|
<AlertDialog
|
|
open={showConfirmDelete}
|
|
onOpenChange={handleDeleteConfirmOpenChange}
|
|
>
|
|
<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', { ns: 'appApi' })}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
|
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
|
|
</AlertDialogDescription>
|
|
</div>
|
|
<AlertDialogActions>
|
|
<AlertDialogCancelButton>
|
|
{t('operation.cancel', { ns: 'common' })}
|
|
</AlertDialogCancelButton>
|
|
<AlertDialogConfirmButton onClick={onDel}>
|
|
{t('operation.confirm', { ns: 'common' })}
|
|
</AlertDialogConfirmButton>
|
|
</AlertDialogActions>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</DialogContent>
|
|
</Dialog>
|
|
{isShow && (
|
|
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setIsVisible(false)} newKey={newKey} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SecretKeyModal
|