index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. 'use client'
  2. import type { MouseEventHandler } from 'react'
  3. import { useMemo, useRef, useState } from 'react'
  4. import { useRouter } from 'next/navigation'
  5. import { useContext } from 'use-context-selector'
  6. import { useTranslation } from 'react-i18next'
  7. import { RiCloseLine } from '@remixicon/react'
  8. import Uploader from './uploader'
  9. import Button from '@/app/components/base/button'
  10. import Input from '@/app/components/base/input'
  11. import Modal from '@/app/components/base/modal'
  12. import { ToastContext } from '@/app/components/base/toast'
  13. import {
  14. importDSL,
  15. importDSLConfirm,
  16. } from '@/service/apps'
  17. import {
  18. DSLImportMode,
  19. DSLImportStatus,
  20. } from '@/models/app'
  21. import { useAppContext } from '@/context/app-context'
  22. import { useProviderContext } from '@/context/provider-context'
  23. import AppsFull from '@/app/components/billing/apps-full-in-dialog'
  24. import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
  25. import { getRedirection } from '@/utils/app-redirection'
  26. import cn from '@/utils/classnames'
  27. import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
  28. type CreateFromDSLModalProps = {
  29. show: boolean
  30. onSuccess?: () => void
  31. onClose: () => void
  32. activeTab?: string
  33. dslUrl?: string
  34. }
  35. export enum CreateFromDSLModalTab {
  36. FROM_FILE = 'from-file',
  37. FROM_URL = 'from-url',
  38. }
  39. const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => {
  40. const { push } = useRouter()
  41. const { t } = useTranslation()
  42. const { notify } = useContext(ToastContext)
  43. const [currentFile, setDSLFile] = useState<File>()
  44. const [fileContent, setFileContent] = useState<string>()
  45. const [currentTab, setCurrentTab] = useState(activeTab)
  46. const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
  47. const [showErrorModal, setShowErrorModal] = useState(false)
  48. const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
  49. const [importId, setImportId] = useState<string>()
  50. const { handleCheckPluginDependencies } = usePluginDependencies()
  51. const readFile = (file: File) => {
  52. const reader = new FileReader()
  53. reader.onload = function (event) {
  54. const content = event.target?.result
  55. setFileContent(content as string)
  56. }
  57. reader.readAsText(file)
  58. }
  59. const handleFile = (file?: File) => {
  60. setDSLFile(file)
  61. if (file)
  62. readFile(file)
  63. if (!file)
  64. setFileContent('')
  65. }
  66. const { isCurrentWorkspaceEditor } = useAppContext()
  67. const { plan, enableBilling } = useProviderContext()
  68. const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
  69. const isCreatingRef = useRef(false)
  70. const onCreate: MouseEventHandler = async () => {
  71. if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
  72. return
  73. if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
  74. return
  75. if (isCreatingRef.current)
  76. return
  77. isCreatingRef.current = true
  78. try {
  79. let response
  80. if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
  81. response = await importDSL({
  82. mode: DSLImportMode.YAML_CONTENT,
  83. yaml_content: fileContent || '',
  84. })
  85. }
  86. if (currentTab === CreateFromDSLModalTab.FROM_URL) {
  87. response = await importDSL({
  88. mode: DSLImportMode.YAML_URL,
  89. yaml_url: dslUrlValue || '',
  90. })
  91. }
  92. if (!response)
  93. return
  94. const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
  95. if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
  96. if (onSuccess)
  97. onSuccess()
  98. if (onClose)
  99. onClose()
  100. notify({
  101. type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
  102. message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
  103. children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
  104. })
  105. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  106. if (app_id)
  107. await handleCheckPluginDependencies(app_id)
  108. getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
  109. }
  110. else if (status === DSLImportStatus.PENDING) {
  111. setVersions({
  112. importedVersion: imported_dsl_version ?? '',
  113. systemVersion: current_dsl_version ?? '',
  114. })
  115. if (onClose)
  116. onClose()
  117. setTimeout(() => {
  118. setShowErrorModal(true)
  119. }, 300)
  120. setImportId(id)
  121. }
  122. else {
  123. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  124. }
  125. }
  126. // eslint-disable-next-line unused-imports/no-unused-vars
  127. catch (e) {
  128. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  129. }
  130. isCreatingRef.current = false
  131. }
  132. const onDSLConfirm: MouseEventHandler = async () => {
  133. try {
  134. if (!importId)
  135. return
  136. const response = await importDSLConfirm({
  137. import_id: importId,
  138. })
  139. const { status, app_id, app_mode } = response
  140. if (status === DSLImportStatus.COMPLETED) {
  141. if (onSuccess)
  142. onSuccess()
  143. if (onClose)
  144. onClose()
  145. notify({
  146. type: 'success',
  147. message: t('app.newApp.appCreated'),
  148. })
  149. if (app_id)
  150. await handleCheckPluginDependencies(app_id)
  151. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  152. getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
  153. }
  154. else if (status === DSLImportStatus.FAILED) {
  155. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  156. }
  157. }
  158. // eslint-disable-next-line unused-imports/no-unused-vars
  159. catch (e) {
  160. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  161. }
  162. }
  163. const tabs = [
  164. {
  165. key: CreateFromDSLModalTab.FROM_FILE,
  166. label: t('app.importFromDSLFile'),
  167. },
  168. {
  169. key: CreateFromDSLModalTab.FROM_URL,
  170. label: t('app.importFromDSLUrl'),
  171. },
  172. ]
  173. const buttonDisabled = useMemo(() => {
  174. if (isAppsFull)
  175. return true
  176. if (currentTab === CreateFromDSLModalTab.FROM_FILE)
  177. return !currentFile
  178. if (currentTab === CreateFromDSLModalTab.FROM_URL)
  179. return !dslUrlValue
  180. return false
  181. }, [isAppsFull, currentTab, currentFile, dslUrlValue])
  182. return (
  183. <>
  184. <Modal
  185. className='w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl'
  186. isShow={show}
  187. onClose={() => { }}
  188. >
  189. <div className='title-2xl-semi-bold flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary'>
  190. {t('app.importFromDSL')}
  191. <div
  192. className='flex h-8 w-8 cursor-pointer items-center'
  193. onClick={() => onClose()}
  194. >
  195. <RiCloseLine className='h-5 w-5 text-text-tertiary' />
  196. </div>
  197. </div>
  198. <div className='system-md-semibold flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary'>
  199. {
  200. tabs.map(tab => (
  201. <div
  202. key={tab.key}
  203. className={cn(
  204. 'relative flex h-full cursor-pointer items-center',
  205. currentTab === tab.key && 'text-text-primary',
  206. )}
  207. onClick={() => setCurrentTab(tab.key)}
  208. >
  209. {tab.label}
  210. {
  211. currentTab === tab.key && (
  212. <div className='absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600'></div>
  213. )
  214. }
  215. </div>
  216. ))
  217. }
  218. </div>
  219. <div className='px-6 py-4'>
  220. {
  221. currentTab === CreateFromDSLModalTab.FROM_FILE && (
  222. <Uploader
  223. className='mt-0'
  224. file={currentFile}
  225. updateFile={handleFile}
  226. />
  227. )
  228. }
  229. {
  230. currentTab === CreateFromDSLModalTab.FROM_URL && (
  231. <div>
  232. <div className='system-md-semibold leading6 mb-1'>DSL URL</div>
  233. <Input
  234. placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
  235. value={dslUrlValue}
  236. onChange={e => setDslUrlValue(e.target.value)}
  237. />
  238. </div>
  239. )
  240. }
  241. </div>
  242. {isAppsFull && (
  243. <div className='px-6'>
  244. <AppsFull className='mt-0' loc='app-create-dsl' />
  245. </div>
  246. )}
  247. <div className='flex justify-end px-6 py-5'>
  248. <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
  249. <Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
  250. </div>
  251. </Modal>
  252. <Modal
  253. isShow={showErrorModal}
  254. onClose={() => setShowErrorModal(false)}
  255. className='w-[480px]'
  256. >
  257. <div className='flex flex-col items-start gap-2 self-stretch pb-4'>
  258. <div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
  259. <div className='system-md-regular flex grow flex-col text-text-secondary'>
  260. <div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
  261. <div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
  262. <br />
  263. <div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
  264. <div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
  265. </div>
  266. </div>
  267. <div className='flex items-start justify-end gap-2 self-stretch pt-6'>
  268. <Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
  269. <Button variant='primary' destructive onClick={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
  270. </div>
  271. </Modal>
  272. </>
  273. )
  274. }
  275. export default CreateFromDSLModal