index.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import type { FC } from 'react'
  2. import {
  3. memo,
  4. useEffect,
  5. useState,
  6. } from 'react'
  7. import { useTranslation } from 'react-i18next'
  8. import {
  9. RiBook2Line,
  10. RiCloseLine,
  11. RiInformation2Line,
  12. RiLock2Fill,
  13. } from '@remixicon/react'
  14. import type { CreateExternalAPIReq, FormSchema } from '../declarations'
  15. import Form from './Form'
  16. import ActionButton from '@/app/components/base/action-button'
  17. import Confirm from '@/app/components/base/confirm'
  18. import {
  19. PortalToFollowElem,
  20. PortalToFollowElemContent,
  21. } from '@/app/components/base/portal-to-follow-elem'
  22. import { createExternalAPI } from '@/service/datasets'
  23. import { useToastContext } from '@/app/components/base/toast'
  24. import Button from '@/app/components/base/button'
  25. import Tooltip from '@/app/components/base/tooltip'
  26. type AddExternalAPIModalProps = {
  27. data?: CreateExternalAPIReq
  28. onSave: (formValue: CreateExternalAPIReq) => void
  29. onCancel: () => void
  30. onEdit?: (formValue: CreateExternalAPIReq) => Promise<void>
  31. datasetBindings?: { id: string; name: string }[]
  32. isEditMode: boolean
  33. }
  34. const formSchemas: FormSchema[] = [
  35. {
  36. variable: 'name',
  37. type: 'text',
  38. label: {
  39. en_US: 'Name',
  40. },
  41. required: true,
  42. },
  43. {
  44. variable: 'endpoint',
  45. type: 'text',
  46. label: {
  47. en_US: 'API Endpoint',
  48. },
  49. required: true,
  50. },
  51. {
  52. variable: 'api_key',
  53. type: 'secret',
  54. label: {
  55. en_US: 'API Key',
  56. },
  57. required: true,
  58. },
  59. ]
  60. const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => {
  61. const { t } = useTranslation()
  62. const { notify } = useToastContext()
  63. const [loading, setLoading] = useState(false)
  64. const [showConfirm, setShowConfirm] = useState(false)
  65. const [formData, setFormData] = useState<CreateExternalAPIReq>({ name: '', settings: { endpoint: '', api_key: '' } })
  66. useEffect(() => {
  67. if (isEditMode && data)
  68. setFormData(data)
  69. }, [isEditMode, data])
  70. const hasEmptyInputs = Object.values(formData).some(value =>
  71. typeof value === 'string' ? value.trim() === '' : Object.values(value).some(v => v.trim() === ''),
  72. )
  73. const handleDataChange = (val: CreateExternalAPIReq) => {
  74. setFormData(val)
  75. }
  76. const handleSave = async () => {
  77. if (formData && formData.settings.api_key && formData.settings.api_key?.length < 5) {
  78. notify({ type: 'error', message: t('common.apiBasedExtension.modal.apiKey.lengthError') })
  79. setLoading(false)
  80. return
  81. }
  82. try {
  83. setLoading(true)
  84. if (isEditMode && onEdit) {
  85. await onEdit(
  86. {
  87. ...formData,
  88. settings: { ...formData.settings, api_key: formData.settings.api_key ? '[__HIDDEN__]' : formData.settings.api_key },
  89. },
  90. )
  91. notify({ type: 'success', message: 'External API updated successfully' })
  92. }
  93. else {
  94. const res = await createExternalAPI({ body: formData })
  95. if (res && res.id) {
  96. notify({ type: 'success', message: 'External API saved successfully' })
  97. onSave(res)
  98. }
  99. }
  100. onCancel()
  101. }
  102. catch (error) {
  103. console.error('Error saving/updating external API:', error)
  104. notify({ type: 'error', message: 'Failed to save/update External API' })
  105. }
  106. finally {
  107. setLoading(false)
  108. }
  109. }
  110. return (
  111. <PortalToFollowElem open>
  112. <PortalToFollowElemContent className='w-full h-full z-[60]'>
  113. <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
  114. <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'>
  115. <div className='flex flex-col pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
  116. <div className='self-stretch text-text-primary title-2xl-semi-bold flex-grow'>
  117. {
  118. isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI')
  119. }
  120. </div>
  121. {isEditMode && (datasetBindings?.length ?? 0) > 0 && (
  122. <div className='text-text-tertiary system-xs-regular flex items-center'>
  123. {t('dataset.editExternalAPIFormWarning.front')}
  124. <span className='text-text-accent cursor-pointer flex items-center'>
  125. &nbsp;{datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}&nbsp;
  126. <Tooltip
  127. popupClassName='flex items-center self-stretch w-[320px]'
  128. popupContent={
  129. <div className='p-1'>
  130. <div className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch'>
  131. <div className='text-text-tertiary system-xs-medium-uppercase'>{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}</div>
  132. </div>
  133. {datasetBindings?.map(binding => (
  134. <div key={binding.id} className='flex px-2 py-1 items-center gap-1 self-stretch'>
  135. <RiBook2Line className='w-4 h-4 text-text-secondary' />
  136. <div className='text-text-secondary system-sm-medium'>{binding.name}</div>
  137. </div>
  138. ))}
  139. </div>
  140. }
  141. asChild={false}
  142. position='bottom'
  143. >
  144. <RiInformation2Line className='w-3.5 h-3.5' />
  145. </Tooltip>
  146. </span>
  147. </div>
  148. )}
  149. </div>
  150. <ActionButton className='absolute top-5 right-5' onClick={onCancel}>
  151. <RiCloseLine className='w-[18px] h-[18px] text-text-tertiary flex-shrink-0' />
  152. </ActionButton>
  153. <Form
  154. value={formData}
  155. onChange={handleDataChange}
  156. formSchemas={formSchemas}
  157. className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'
  158. />
  159. <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
  160. <Button type='button' variant='secondary' onClick={onCancel}>
  161. {t('dataset.externalAPIForm.cancel')}
  162. </Button>
  163. <Button
  164. type='submit'
  165. variant='primary'
  166. onClick={() => {
  167. if (isEditMode && (datasetBindings?.length ?? 0) > 0)
  168. setShowConfirm(true)
  169. else if (isEditMode && onEdit)
  170. onEdit(formData)
  171. else
  172. handleSave()
  173. }}
  174. disabled={hasEmptyInputs || loading}
  175. >
  176. {t('dataset.externalAPIForm.save')}
  177. </Button>
  178. </div>
  179. <div className='flex px-2 py-3 justify-center items-center gap-1 self-stretch rounded-b-2xl
  180. border-t-[0.5px] border-divider-subtle bg-background-soft text-text-tertiary system-xs-regular'
  181. >
  182. <RiLock2Fill className='w-3 h-3 text-text-quaternary' />
  183. {t('dataset.externalAPIForm.encrypted.front')}
  184. <a
  185. className='text-text-accent'
  186. target='_blank' rel='noopener noreferrer'
  187. href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
  188. >
  189. PKCS1_OAEP
  190. </a>
  191. {t('dataset.externalAPIForm.encrypted.end')}
  192. </div>
  193. </div>
  194. {showConfirm && (datasetBindings?.length ?? 0) > 0 && (
  195. <Confirm
  196. isShow={showConfirm}
  197. type='warning'
  198. title='Warning'
  199. content={`${t('dataset.editExternalAPIConfirmWarningContent.front')} ${datasetBindings?.length} ${t('dataset.editExternalAPIConfirmWarningContent.end')}`}
  200. onCancel={() => setShowConfirm(false)}
  201. onConfirm={handleSave}
  202. />
  203. )}
  204. </div>
  205. </PortalToFollowElemContent>
  206. </PortalToFollowElem>
  207. )
  208. }
  209. export default memo(AddExternalAPIModal)