| 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) |
|
|