You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 11KB

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