import type { OffsetOptions } from '@floating-ui/react' import type { Placement } from '@langgenius/dify-ui/popover' import type { Credential, PluginPayload } from '../types' import { AlertDialog, AlertDialogActions, AlertDialogCancelButton, AlertDialogConfirmButton, AlertDialogContent, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import Authorize from '../authorize' import ApiKeyModal from '../authorize/api-key-modal' import { useDeletePluginCredentialHook, useSetPluginDefaultCredentialHook, useUpdatePluginCredentialHook, } from '../hooks/use-credential' import { CredentialTypeEnum } from '../types' import Item from './item' type AuthorizedProps = { pluginPayload: PluginPayload credentials: Credential[] canOAuth?: boolean canApiKey?: boolean renderTrigger?: (open?: boolean) => React.ReactNode isOpen?: boolean onOpenChange?: (open: boolean) => void offset?: number | OffsetOptions placement?: Placement triggerPopupSameWidth?: boolean popupClassName?: string disableSetDefault?: boolean onItemClick?: (id: string) => void extraAuthorizationItems?: Credential[] showItemSelectedIcon?: boolean selectedCredentialId?: string onUpdate?: () => void notAllowCustomCredential?: boolean } const Authorized = ({ pluginPayload, credentials, canOAuth, canApiKey, renderTrigger, isOpen, onOpenChange, offset = 8, placement = 'bottom-start', triggerPopupSameWidth = true, popupClassName, disableSetDefault, onItemClick, extraAuthorizationItems, showItemSelectedIcon, selectedCredentialId, onUpdate, notAllowCustomCredential, }: AuthorizedProps) => { const { t } = useTranslation() const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() const [isLocalOpen, setIsLocalOpen] = useState(false) const mergedIsOpen = isOpen ?? isLocalOpen const setMergedIsOpen = useCallback( (open: boolean) => { if (onOpenChange) onOpenChange(open) setIsLocalOpen(open) }, [onOpenChange], ) const oAuthCredentials = credentials.filter( (credential) => credential.credential_type === CredentialTypeEnum.OAUTH2, ) const apiKeyCredentials = credentials.filter( (credential) => credential.credential_type === CredentialTypeEnum.API_KEY, ) const pendingOperationCredentialIdRef = useRef(null) const [deleteCredentialId, setDeleteCredentialId] = useState(null) const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload) const openConfirm = useCallback( (credentialId?: string) => { if (!canManageCredential) return setMergedIsOpen(false) if (credentialId) pendingOperationCredentialIdRef.current = credentialId setDeleteCredentialId(pendingOperationCredentialIdRef.current) }, [canManageCredential, setMergedIsOpen], ) const closeConfirm = useCallback(() => { setDeleteCredentialId(null) pendingOperationCredentialIdRef.current = null }, []) const [doingAction, setDoingAction] = useState(false) const doingActionRef = useRef(doingAction) const handleSetDoingAction = useCallback((doing: boolean) => { doingActionRef.current = doing setDoingAction(doing) }, []) const handleConfirm = useCallback(async () => { if (doingActionRef.current || !canManageCredential) return if (!pendingOperationCredentialIdRef.current) { setDeleteCredentialId(null) return } try { handleSetDoingAction(true) await deletePluginCredential({ credential_id: pendingOperationCredentialIdRef.current }) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onUpdate?.() setDeleteCredentialId(null) pendingOperationCredentialIdRef.current = null } finally { handleSetDoingAction(false) } }, [canManageCredential, deletePluginCredential, onUpdate, t, handleSetDoingAction]) const [editValues, setEditValues] = useState | null>(null) const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false) const handleEdit = useCallback( (id: string, values: Record) => { if (!canManageCredential) return setMergedIsOpen(false) pendingOperationCredentialIdRef.current = id setEditValues(values) setIsApiKeyModalOpen(true) }, [canManageCredential, setMergedIsOpen], ) const handleApiKeyModalOpenChange = useCallback((open: boolean) => { setIsApiKeyModalOpen(open) if (!open) pendingOperationCredentialIdRef.current = null }, []) // Lifted state for the "+ Add API Key" modal so it isn't unmounted when the // popover closes due to outside-click detection on the modal's portal. const [isAddApiKeyOpen, setIsAddApiKeyOpen] = useState(false) const handleAddApiKeyClick = useCallback(() => { if (!canCreateCredential) return setMergedIsOpen(false) setIsAddApiKeyOpen(true) }, [canCreateCredential, setMergedIsOpen]) const handleRemove = useCallback(() => { if (!canManageCredential) return setDeleteCredentialId(pendingOperationCredentialIdRef.current) }, [canManageCredential]) const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload) const handleSetDefault = useCallback( async (id: string) => { if (doingActionRef.current || !canUseCredential) return try { handleSetDoingAction(true) await setPluginDefaultCredential(id) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onUpdate?.() } finally { handleSetDoingAction(false) } }, [canUseCredential, setPluginDefaultCredential, onUpdate, t, handleSetDoingAction], ) const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) const handleRename = useCallback( async (payload: { credential_id: string; name: string }) => { if (doingActionRef.current || !canManageCredential) return try { handleSetDoingAction(true) await updatePluginCredential(payload) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onUpdate?.() } finally { handleSetDoingAction(false) } }, [canManageCredential, updatePluginCredential, t, handleSetDoingAction, onUpdate], ) const unavailableCredentials = credentials.filter((credential) => credential.not_allowed_to_use) const unavailableCredential = credentials.find( (credential) => credential.not_allowed_to_use && credential.is_default, ) const resolvedOffset = typeof offset === 'number' || typeof offset === 'function' ? undefined : offset const sideOffset = typeof offset === 'number' ? offset : (resolvedOffset?.mainAxis ?? 0) const alignOffset = typeof offset === 'number' ? 0 : (resolvedOffset?.crossAxis ?? resolvedOffset?.alignmentAxis ?? 0) const popupProps = triggerPopupSameWidth ? { style: { width: 'var(--anchor-width, auto)' } } : undefined return ( <> {renderTrigger ? ( renderTrigger(mergedIsOpen) ) : ( )} } />
{!!extraAuthorizationItems?.length && (
{extraAuthorizationItems.map((credential) => ( ))}
)} {!!oAuthCredentials.length && (
OAuth
{oAuthCredentials.map((credential) => ( ))}
)} {!!apiKeyCredentials.length && (
API Keys
{apiKeyCredentials.map((credential) => ( ))}
)}
{!notAllowCustomCredential && ( <>
)}
!open && closeConfirm()}>
{t(($) => $['list.delete.title'], { ns: 'datasetDocuments' })}
{t(($) => $['operation.cancel'], { ns: 'common' })} {t(($) => $['operation.confirm'], { ns: 'common' })}
{!!editValues && ( handleApiKeyModalOpenChange(false)} onRemove={handleRemove} disabled={!canManageCredential || doingAction} onUpdate={onUpdate} /> )} {isAddApiKeyOpen && ( setIsAddApiKeyOpen(false)} disabled={!canCreateCredential || doingAction} onUpdate={onUpdate} /> )} ) } export default memo(Authorized)