mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-31 10:53:02 +00:00 
			
		
		
		
	
		
			
	
	
		
			219 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			219 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|   | import type { FC } from 'react' | ||
|  | import { | ||
|  |   memo, | ||
|  |   useEffect, | ||
|  |   useState, | ||
|  | } from 'react' | ||
|  | import { useTranslation } from 'react-i18next' | ||
|  | import { | ||
|  |   RiBook2Line, | ||
|  |   RiCloseLine, | ||
|  |   RiInformation2Line, | ||
|  |   RiLock2Fill, | ||
|  | } from '@remixicon/react' | ||
|  | import type { CreateExternalAPIReq, FormSchema } from '../declarations' | ||
|  | import Form from './Form' | ||
|  | import ActionButton from '@/app/components/base/action-button' | ||
|  | import Confirm from '@/app/components/base/confirm' | ||
|  | import { | ||
|  |   PortalToFollowElem, | ||
|  |   PortalToFollowElemContent, | ||
|  | } from '@/app/components/base/portal-to-follow-elem' | ||
|  | import { createExternalAPI } from '@/service/datasets' | ||
|  | import { useToastContext } from '@/app/components/base/toast' | ||
|  | import Button from '@/app/components/base/button' | ||
|  | import Tooltip from '@/app/components/base/tooltip' | ||
|  | 
 | ||
|  | type AddExternalAPIModalProps = { | ||
|  |   data?: CreateExternalAPIReq | ||
|  |   onSave: (formValue: CreateExternalAPIReq) => void | ||
|  |   onCancel: () => void | ||
|  |   onEdit?: (formValue: CreateExternalAPIReq) => Promise<void> | ||
|  |   datasetBindings?: { id: string; name: string }[] | ||
|  |   isEditMode: boolean | ||
|  | } | ||
|  | 
 | ||
|  | const formSchemas: FormSchema[] = [ | ||
|  |   { | ||
|  |     variable: 'name', | ||
|  |     type: 'text', | ||
|  |     label: { | ||
|  |       en_US: 'Name', | ||
|  |     }, | ||
|  |     required: true, | ||
|  |   }, | ||
|  |   { | ||
|  |     variable: 'endpoint', | ||
|  |     type: 'text', | ||
|  |     label: { | ||
|  |       en_US: 'API Endpoint', | ||
|  |     }, | ||
|  |     required: true, | ||
|  |   }, | ||
|  |   { | ||
|  |     variable: 'api_key', | ||
|  |     type: 'secret', | ||
|  |     label: { | ||
|  |       en_US: 'API Key', | ||
|  |     }, | ||
|  |     required: true, | ||
|  |   }, | ||
|  | ] | ||
|  | 
 | ||
|  | const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => { | ||
|  |   const { t } = useTranslation() | ||
|  |   const { notify } = useToastContext() | ||
|  |   const [loading, setLoading] = useState(false) | ||
|  |   const [showConfirm, setShowConfirm] = useState(false) | ||
|  |   const [formData, setFormData] = useState<CreateExternalAPIReq>({ name: '', settings: { endpoint: '', api_key: '' } }) | ||
|  | 
 | ||
|  |   useEffect(() => { | ||
|  |     if (isEditMode && data) | ||
|  |       setFormData(data) | ||
|  |   }, [isEditMode, data]) | ||
|  | 
 | ||
|  |   const hasEmptyInputs = Object.values(formData).some(value => | ||
|  |     typeof value === 'string' ? value.trim() === '' : Object.values(value).some(v => v.trim() === ''), | ||
|  |   ) | ||
|  |   const handleDataChange = (val: CreateExternalAPIReq) => { | ||
|  |     setFormData(val) | ||
|  |   } | ||
|  | 
 | ||
|  |   const handleSave = async () => { | ||
|  |     if (formData && formData.settings.api_key && formData.settings.api_key?.length < 5) { | ||
|  |       notify({ type: 'error', message: t('common.apiBasedExtension.modal.apiKey.lengthError') }) | ||
|  |       setLoading(false) | ||
|  |       return | ||
|  |     } | ||
|  |     try { | ||
|  |       setLoading(true) | ||
|  |       if (isEditMode && onEdit) { | ||
|  |         await onEdit( | ||
|  |           { | ||
|  |             ...formData, | ||
|  |             settings: { ...formData.settings, api_key: formData.settings.api_key ? '[__HIDDEN__]' : formData.settings.api_key }, | ||
|  |           }, | ||
|  |         ) | ||
|  |         notify({ type: 'success', message: 'External API updated successfully' }) | ||
|  |       } | ||
|  |       else { | ||
|  |         const res = await createExternalAPI({ body: formData }) | ||
|  |         if (res && res.id) { | ||
|  |           notify({ type: 'success', message: 'External API saved successfully' }) | ||
|  |           onSave(res) | ||
|  |         } | ||
|  |       } | ||
|  |       onCancel() | ||
|  |     } | ||
|  |     catch (error) { | ||
|  |       console.error('Error saving/updating external API:', error) | ||
|  |       notify({ type: 'error', message: 'Failed to save/update External API' }) | ||
|  |     } | ||
|  |     finally { | ||
|  |       setLoading(false) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return ( | ||
|  |     <PortalToFollowElem open> | ||
|  |       <PortalToFollowElemContent className='w-full h-full z-[60]'> | ||
|  |         <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> | ||
|  |           <div className='flex relative w-[480px] flex-col items-start bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadows-shadow-xl'> | ||
|  |             <div className='flex flex-col pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> | ||
|  |               <div className='self-stretch text-text-primary title-2xl-semi-bold flex-grow'> | ||
|  |                 { | ||
|  |                   isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI') | ||
|  |                 } | ||
|  |               </div> | ||
|  |               {isEditMode && (datasetBindings?.length ?? 0) > 0 && ( | ||
|  |                 <div className='text-text-tertiary system-xs-regular flex items-center'> | ||
|  |                   {t('dataset.editExternalAPIFormWarning.front')} | ||
|  |                   <span className='text-text-accent cursor-pointer flex items-center'> | ||
|  |                      {datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}  | ||
|  |                     <Tooltip | ||
|  |                       popupClassName='flex items-center self-stretch w-[320px]' | ||
|  |                       popupContent={ | ||
|  |                         <div className='p-1'> | ||
|  |                           <div className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch'> | ||
|  |                             <div className='text-text-tertiary system-xs-medium-uppercase'>{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}</div> | ||
|  |                           </div> | ||
|  |                           {datasetBindings?.map(binding => ( | ||
|  |                             <div key={binding.id} className='flex px-2 py-1 items-center gap-1 self-stretch'> | ||
|  |                               <RiBook2Line className='w-4 h-4 text-text-secondary' /> | ||
|  |                               <div className='text-text-secondary system-sm-medium'>{binding.name}</div> | ||
|  |                             </div> | ||
|  |                           ))} | ||
|  |                         </div> | ||
|  |                       } | ||
|  |                       asChild={false} | ||
|  |                       position='bottom' | ||
|  |                     > | ||
|  |                       <RiInformation2Line className='w-3.5 h-3.5' /> | ||
|  |                     </Tooltip> | ||
|  |                   </span> | ||
|  |                 </div> | ||
|  |               )} | ||
|  |             </div> | ||
|  |             <ActionButton className='absolute top-5 right-5' onClick={onCancel}> | ||
|  |               <RiCloseLine className='w-[18px] h-[18px] text-text-tertiary flex-shrink-0' /> | ||
|  |             </ActionButton> | ||
|  |             <Form | ||
|  |               value={formData} | ||
|  |               onChange={handleDataChange} | ||
|  |               formSchemas={formSchemas} | ||
|  |               className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch' | ||
|  |             /> | ||
|  |             <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> | ||
|  |               <Button type='button' variant='secondary' onClick={onCancel}> | ||
|  |                 {t('dataset.externalAPIForm.cancel')} | ||
|  |               </Button> | ||
|  |               <Button | ||
|  |                 type='submit' | ||
|  |                 variant='primary' | ||
|  |                 onClick={() => { | ||
|  |                   if (isEditMode && (datasetBindings?.length ?? 0) > 0) | ||
|  |                     setShowConfirm(true) | ||
|  |                   else if (isEditMode && onEdit) | ||
|  |                     onEdit(formData) | ||
|  | 
 | ||
|  |                   else | ||
|  |                     handleSave() | ||
|  |                 }} | ||
|  |                 disabled={hasEmptyInputs || loading} | ||
|  |               > | ||
|  |                 {t('dataset.externalAPIForm.save')} | ||
|  |               </Button> | ||
|  |             </div> | ||
|  |             <div className='flex px-2 py-3 justify-center items-center gap-1 self-stretch rounded-b-2xl | ||
|  |               border-t-[0.5px] border-divider-subtle bg-background-soft text-text-tertiary system-xs-regular' | ||
|  |             > | ||
|  |               <RiLock2Fill className='w-3 h-3 text-text-quaternary' /> | ||
|  |               {t('dataset.externalAPIForm.encrypted.front')} | ||
|  |               <a | ||
|  |                 className='text-text-accent' | ||
|  |                 target='_blank' rel='noopener noreferrer' | ||
|  |                 href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' | ||
|  |               > | ||
|  |                 PKCS1_OAEP | ||
|  |               </a> | ||
|  |               {t('dataset.externalAPIForm.encrypted.end')} | ||
|  |             </div> | ||
|  |           </div> | ||
|  |           {showConfirm && (datasetBindings?.length ?? 0) > 0 && ( | ||
|  |             <Confirm | ||
|  |               isShow={showConfirm} | ||
|  |               type='warning' | ||
|  |               title='Warning' | ||
|  |               content={`${t('dataset.editExternalAPIConfirmWarningContent.front')} ${datasetBindings?.length} ${t('dataset.editExternalAPIConfirmWarningContent.end')}`} | ||
|  |               onCancel={() => setShowConfirm(false)} | ||
|  |               onConfirm={handleSave} | ||
|  |             /> | ||
|  |           )} | ||
|  |         </div> | ||
|  |       </PortalToFollowElemContent> | ||
|  |     </PortalToFollowElem> | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | export default memo(AddExternalAPIModal) |