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