| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- import {
- memo,
- useCallback,
- useRef,
- useState,
- } from 'react'
- import {
- RiArrowDownSLine,
- } from '@remixicon/react'
- import { useTranslation } from 'react-i18next'
- import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
- } from '@/app/components/base/portal-to-follow-elem'
- import type {
- PortalToFollowElemOptions,
- } from '@/app/components/base/portal-to-follow-elem'
- import Button from '@/app/components/base/button'
- import Indicator from '@/app/components/header/indicator'
- import cn from '@/utils/classnames'
- import Confirm from '@/app/components/base/confirm'
- import Authorize from '../authorize'
- import type { Credential } from '../types'
- import { CredentialTypeEnum } from '../types'
- import ApiKeyModal from '../authorize/api-key-modal'
- import Item from './item'
- import { useToastContext } from '@/app/components/base/toast'
- import type { PluginPayload } from '../types'
- import {
- useDeletePluginCredentialHook,
- useSetPluginDefaultCredentialHook,
- useUpdatePluginCredentialHook,
- } from '../hooks/use-credential'
-
- type AuthorizedProps = {
- pluginPayload: PluginPayload
- credentials: Credential[]
- canOAuth?: boolean
- canApiKey?: boolean
- disabled?: boolean
- renderTrigger?: (open?: boolean) => React.ReactNode
- isOpen?: boolean
- onOpenChange?: (open: boolean) => void
- offset?: PortalToFollowElemOptions['offset']
- placement?: PortalToFollowElemOptions['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,
- disabled,
- renderTrigger,
- isOpen,
- onOpenChange,
- offset = 8,
- placement = 'bottom-start',
- triggerPopupSameWidth = true,
- popupClassName,
- disableSetDefault,
- onItemClick,
- extraAuthorizationItems,
- showItemSelectedIcon,
- selectedCredentialId,
- onUpdate,
- notAllowCustomCredential,
- }: AuthorizedProps) => {
- const { t } = useTranslation()
- const { notify } = useToastContext()
- 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 pendingOperationCredentialId = useRef<string | null>(null)
- const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
- const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
- const openConfirm = useCallback((credentialId?: string) => {
- if (credentialId)
- pendingOperationCredentialId.current = credentialId
-
- setDeleteCredentialId(pendingOperationCredentialId.current)
- }, [])
- const closeConfirm = useCallback(() => {
- setDeleteCredentialId(null)
- pendingOperationCredentialId.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)
- return
- if (!pendingOperationCredentialId.current) {
- setDeleteCredentialId(null)
- return
- }
- try {
- handleSetDoingAction(true)
- await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
- notify({
- type: 'success',
- message: t('common.api.actionSuccess'),
- })
- onUpdate?.()
- setDeleteCredentialId(null)
- pendingOperationCredentialId.current = null
- }
- finally {
- handleSetDoingAction(false)
- }
- }, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
- const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
- const handleEdit = useCallback((id: string, values: Record<string, any>) => {
- pendingOperationCredentialId.current = id
- setEditValues(values)
- }, [])
- const handleRemove = useCallback(() => {
- setDeleteCredentialId(pendingOperationCredentialId.current)
- }, [])
- const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
- const handleSetDefault = useCallback(async (id: string) => {
- if (doingActionRef.current)
- return
- try {
- handleSetDoingAction(true)
- await setPluginDefaultCredential(id)
- notify({
- type: 'success',
- message: t('common.api.actionSuccess'),
- })
- onUpdate?.()
- }
- finally {
- handleSetDoingAction(false)
- }
- }, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
- const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
- const handleRename = useCallback(async (payload: {
- credential_id: string
- name: string
- }) => {
- if (doingActionRef.current)
- return
- try {
- handleSetDoingAction(true)
- await updatePluginCredential(payload)
- notify({
- type: 'success',
- message: t('common.api.actionSuccess'),
- })
- onUpdate?.()
- }
- finally {
- handleSetDoingAction(false)
- }
- }, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
- const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
-
- return (
- <>
- <PortalToFollowElem
- open={mergedIsOpen}
- onOpenChange={setMergedIsOpen}
- placement={placement}
- offset={offset}
- triggerPopupSameWidth={triggerPopupSameWidth}
- >
- <PortalToFollowElemTrigger
- onClick={() => setMergedIsOpen(!mergedIsOpen)}
- asChild
- >
- {
- renderTrigger
- ? renderTrigger(mergedIsOpen)
- : (
- <Button
- className={cn(
- 'w-full',
- isOpen && 'bg-components-button-secondary-bg-hover',
- )}>
- <Indicator className='mr-2' />
- {credentials.length}
- {
- credentials.length > 1
- ? t('plugin.auth.authorizations')
- : t('plugin.auth.authorization')
- }
- {
- !!unavailableCredentials.length && (
- ` (${unavailableCredentials.length} ${t('plugin.auth.unavailable')})`
- )
- }
- <RiArrowDownSLine className='ml-0.5 h-4 w-4' />
- </Button>
- )
- }
- </PortalToFollowElemTrigger>
- <PortalToFollowElemContent className='z-[100]'>
- <div className={cn(
- 'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
- popupClassName,
- )}>
- <div className='py-1'>
- {
- !!extraAuthorizationItems?.length && (
- <div className='p-1'>
- {
- extraAuthorizationItems.map(credential => (
- <Item
- key={credential.id}
- credential={credential}
- disabled={disabled}
- onItemClick={onItemClick}
- disableRename
- disableEdit
- disableDelete
- disableSetDefault
- showSelectedIcon={showItemSelectedIcon}
- selectedCredentialId={selectedCredentialId}
- />
- ))
- }
- </div>
- )
- }
- {
- !!oAuthCredentials.length && (
- <div className='p-1'>
- <div className={cn(
- 'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
- showItemSelectedIcon && 'pl-7',
- )}>
- OAuth
- </div>
- {
- oAuthCredentials.map(credential => (
- <Item
- key={credential.id}
- credential={credential}
- disabled={disabled}
- disableEdit
- onDelete={openConfirm}
- onSetDefault={handleSetDefault}
- onRename={handleRename}
- disableSetDefault={disableSetDefault}
- onItemClick={onItemClick}
- showSelectedIcon={showItemSelectedIcon}
- selectedCredentialId={selectedCredentialId}
- />
- ))
- }
- </div>
- )
- }
- {
- !!apiKeyCredentials.length && (
- <div className='p-1'>
- <div className={cn(
- 'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
- showItemSelectedIcon && 'pl-7',
- )}>
- API Keys
- </div>
- {
- apiKeyCredentials.map(credential => (
- <Item
- key={credential.id}
- credential={credential}
- disabled={disabled}
- onDelete={openConfirm}
- onEdit={handleEdit}
- onSetDefault={handleSetDefault}
- disableSetDefault={disableSetDefault}
- disableRename
- onItemClick={onItemClick}
- onRename={handleRename}
- showSelectedIcon={showItemSelectedIcon}
- selectedCredentialId={selectedCredentialId}
- />
- ))
- }
- </div>
- )
- }
- </div>
- {
- !notAllowCustomCredential && (
- <>
- <div className='h-[1px] bg-divider-subtle'></div>
- <div className='p-2'>
- <Authorize
- pluginPayload={pluginPayload}
- theme='secondary'
- showDivider={false}
- canOAuth={canOAuth}
- canApiKey={canApiKey}
- disabled={disabled}
- onUpdate={onUpdate}
- />
- </div>
- </>
- )
- }
- </div>
- </PortalToFollowElemContent>
- </PortalToFollowElem>
- {
- deleteCredentialId && (
- <Confirm
- isShow
- title={t('datasetDocuments.list.delete.title')}
- isDisabled={doingAction}
- onCancel={closeConfirm}
- onConfirm={handleConfirm}
- />
- )
- }
- {
- !!editValues && (
- <ApiKeyModal
- pluginPayload={pluginPayload}
- editValues={editValues}
- onClose={() => {
- setEditValues(null)
- pendingOperationCredentialId.current = null
- }}
- onRemove={handleRemove}
- disabled={disabled || doingAction}
- onUpdate={onUpdate}
- />
- )
- }
- </>
- )
- }
-
- export default memo(Authorized)
|