Browse Source

feat(frontend): workflow import dsl from url (#6286)

zxhlyh 9 months ago
parent
commit
9a536979ab

+ 26 - 4
web/app/(commonLayout)/apps/NewAppCard.tsx

@@ -1,10 +1,14 @@
 'use client'
 'use client'
 
 
-import { forwardRef, useState } from 'react'
+import { forwardRef, useMemo, useState } from 'react'
+import {
+  useRouter,
+  useSearchParams,
+} from 'next/navigation'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
 import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
 import CreateAppModal from '@/app/components/app/create-app-modal'
 import CreateAppModal from '@/app/components/app/create-app-modal'
-import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
+import CreateFromDSLModal, { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
 import { useProviderContext } from '@/context/provider-context'
 import { useProviderContext } from '@/context/provider-context'
 import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
 import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
 
 
@@ -16,10 +20,21 @@ export type CreateAppCardProps = {
 const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
 const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { onPlanInfoChanged } = useProviderContext()
   const { onPlanInfoChanged } = useProviderContext()
+  const searchParams = useSearchParams()
+  const { replace } = useRouter()
+  const dslUrl = searchParams.get('remoteInstallUrl') || undefined
 
 
   const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
   const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
   const [showNewAppModal, setShowNewAppModal] = useState(false)
   const [showNewAppModal, setShowNewAppModal] = useState(false)
-  const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
+  const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(!!dslUrl)
+
+  const activeTab = useMemo(() => {
+    if (dslUrl)
+      return CreateFromDSLModalTab.FROM_URL
+
+    return undefined
+  }, [dslUrl])
+
   return (
   return (
     <a
     <a
       ref={ref}
       ref={ref}
@@ -65,7 +80,14 @@ const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuc
       />
       />
       <CreateFromDSLModal
       <CreateFromDSLModal
         show={showCreateFromDSLModal}
         show={showCreateFromDSLModal}
-        onClose={() => setShowCreateFromDSLModal(false)}
+        onClose={() => {
+          setShowCreateFromDSLModal(false)
+
+          if (dslUrl)
+            replace('/')
+        }}
+        activeTab={activeTab}
+        dslUrl={dslUrl}
         onSuccess={() => {
         onSuccess={() => {
           onPlanInfoChanged()
           onPlanInfoChanged()
           if (onSuccess)
           if (onSuccess)

+ 114 - 19
web/app/components/app/create-from-dsl-modal/index.tsx

@@ -1,7 +1,7 @@
 'use client'
 'use client'
 
 
 import type { MouseEventHandler } from 'react'
 import type { MouseEventHandler } from 'react'
-import { useRef, useState } from 'react'
+import { useMemo, useRef, useState } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import { useContext } from 'use-context-selector'
 import { useContext } from 'use-context-selector'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
@@ -10,25 +10,38 @@ import Uploader from './uploader'
 import Button from '@/app/components/base/button'
 import Button from '@/app/components/base/button'
 import Modal from '@/app/components/base/modal'
 import Modal from '@/app/components/base/modal'
 import { ToastContext } from '@/app/components/base/toast'
 import { ToastContext } from '@/app/components/base/toast'
-import { importApp } from '@/service/apps'
+import {
+  importApp,
+  importAppFromUrl,
+} from '@/service/apps'
 import { useAppContext } from '@/context/app-context'
 import { useAppContext } from '@/context/app-context'
 import { useProviderContext } from '@/context/provider-context'
 import { useProviderContext } from '@/context/provider-context'
 import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import { getRedirection } from '@/utils/app-redirection'
 import { getRedirection } from '@/utils/app-redirection'
+import cn from '@/utils/classnames'
 
 
 type CreateFromDSLModalProps = {
 type CreateFromDSLModalProps = {
   show: boolean
   show: boolean
   onSuccess?: () => void
   onSuccess?: () => void
   onClose: () => void
   onClose: () => void
+  activeTab?: string
+  dslUrl?: string
 }
 }
 
 
-const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProps) => {
+export enum CreateFromDSLModalTab {
+  FROM_FILE = 'from-file',
+  FROM_URL = 'from-url',
+}
+
+const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => {
   const { push } = useRouter()
   const { push } = useRouter()
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
   const { notify } = useContext(ToastContext)
   const [currentFile, setDSLFile] = useState<File>()
   const [currentFile, setDSLFile] = useState<File>()
   const [fileContent, setFileContent] = useState<string>()
   const [fileContent, setFileContent] = useState<string>()
+  const [currentTab, setCurrentTab] = useState(activeTab)
+  const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
 
 
   const readFile = (file: File) => {
   const readFile = (file: File) => {
     const reader = new FileReader()
     const reader = new FileReader()
@@ -53,15 +66,26 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp
 
 
   const isCreatingRef = useRef(false)
   const isCreatingRef = useRef(false)
   const onCreate: MouseEventHandler = async () => {
   const onCreate: MouseEventHandler = async () => {
+    if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
+      return
+    if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
+      return
     if (isCreatingRef.current)
     if (isCreatingRef.current)
       return
       return
     isCreatingRef.current = true
     isCreatingRef.current = true
-    if (!currentFile)
-      return
     try {
     try {
-      const app = await importApp({
-        data: fileContent || '',
-      })
+      let app
+
+      if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
+        app = await importApp({
+          data: fileContent || '',
+        })
+      }
+      if (currentTab === CreateFromDSLModalTab.FROM_URL) {
+        app = await importAppFromUrl({
+          url: dslUrlValue || '',
+        })
+      }
       if (onSuccess)
       if (onSuccess)
         onSuccess()
         onSuccess()
       if (onClose)
       if (onClose)
@@ -76,24 +100,95 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp
     isCreatingRef.current = false
     isCreatingRef.current = false
   }
   }
 
 
+  const tabs = [
+    {
+      key: CreateFromDSLModalTab.FROM_FILE,
+      label: t('app.importFromDSLFile'),
+    },
+    {
+      key: CreateFromDSLModalTab.FROM_URL,
+      label: t('app.importFromDSLUrl'),
+    },
+  ]
+
+  const buttonDisabled = useMemo(() => {
+    if (isAppsFull)
+      return true
+    if (currentTab === CreateFromDSLModalTab.FROM_FILE)
+      return !currentFile
+    if (currentTab === CreateFromDSLModalTab.FROM_URL)
+      return !dslUrlValue
+    return false
+  }, [isAppsFull, currentTab, currentFile, dslUrlValue])
+
   return (
   return (
     <Modal
     <Modal
-      className='px-8 py-6 max-w-[520px] w-[520px] rounded-xl'
+      className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
       isShow={show}
       isShow={show}
       onClose={() => { }}
       onClose={() => { }}
     >
     >
-      <div className='relative pb-2 text-xl font-medium leading-[30px] text-gray-900'>{t('app.createFromConfigFile')}</div>
-      <div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
-        <RiCloseLine className='w-4 h-4 text-gray-500' />
+      <div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
+        {t('app.importFromDSL')}
+        <div
+          className='flex items-center w-8 h-8 cursor-pointer'
+          onClick={() => onClose()}
+        >
+          <RiCloseLine className='w-5 h-5 text-text-tertiary' />
+        </div>
+      </div>
+      <div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
+        {
+          tabs.map(tab => (
+            <div
+              key={tab.key}
+              className={cn(
+                'relative flex items-center h-full cursor-pointer',
+                currentTab === tab.key && 'text-text-primary',
+              )}
+              onClick={() => setCurrentTab(tab.key)}
+            >
+              {tab.label}
+              {
+                currentTab === tab.key && (
+                  <div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
+                )
+              }
+            </div>
+          ))
+        }
+      </div>
+      <div className='px-6 py-4'>
+        {
+          currentTab === CreateFromDSLModalTab.FROM_FILE && (
+            <Uploader
+              className='mt-0'
+              file={currentFile}
+              updateFile={handleFile}
+            />
+          )
+        }
+        {
+          currentTab === CreateFromDSLModalTab.FROM_URL && (
+            <div>
+              <div className='mb-1 system-md-semibold leading6'>DSL URL</div>
+              <input
+                placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
+                className='px-2 w-full h-8 border border-components-input-border-active bg-components-input-bg-active rounded-lg outline-none appearance-none placeholder:text-components-input-text-placeholder system-sm-regular'
+                value={dslUrlValue}
+                onChange={e => setDslUrlValue(e.target.value)}
+              />
+            </div>
+          )
+        }
       </div>
       </div>
-      <Uploader
-        file={currentFile}
-        updateFile={handleFile}
-      />
-      {isAppsFull && <AppsFull loc='app-create-dsl' />}
-      <div className='pt-6 flex justify-end'>
+      {isAppsFull && (
+        <div className='px-6'>
+          <AppsFull className='mt-0' loc='app-create-dsl' />
+        </div>
+      )}
+      <div className='flex justify-end px-6 py-5'>
         <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
         <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
-        <Button disabled={isAppsFull || !currentFile} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
+        <Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
       </div>
       </div>
     </Modal>
     </Modal>
   )
   )

+ 6 - 2
web/app/components/billing/apps-full-in-dialog/index.tsx

@@ -8,14 +8,18 @@ import s from './style.module.css'
 import cn from '@/utils/classnames'
 import cn from '@/utils/classnames'
 import GridMask from '@/app/components/base/grid-mask'
 import GridMask from '@/app/components/base/grid-mask'
 
 
-const AppsFull: FC<{ loc: string }> = ({
+const AppsFull: FC<{ loc: string; className?: string }> = ({
   loc,
   loc,
+  className,
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
   return (
   return (
     <GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
     <GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
-      <div className='mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
+      <div className={cn(
+        'mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer',
+        className,
+      )}>
         <div className='flex justify-between items-center'>
         <div className='flex justify-between items-center'>
           <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
           <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
             <div>{t('billing.apps.fullTipLine1')}</div>
             <div>{t('billing.apps.fullTipLine1')}</div>

+ 4 - 0
web/i18n/en-US/app.ts

@@ -13,6 +13,10 @@ const translation = {
   exportFailed: 'Export DSL failed.',
   exportFailed: 'Export DSL failed.',
   importDSL: 'Import DSL file',
   importDSL: 'Import DSL file',
   createFromConfigFile: 'Create from DSL file',
   createFromConfigFile: 'Create from DSL file',
+  importFromDSL: 'Import from DSL',
+  importFromDSLFile: 'From DSL file',
+  importFromDSLUrl: 'From URL',
+  importFromDSLUrlPlaceholder: 'Paste DSL link here',
   deleteAppConfirmTitle: 'Delete this app?',
   deleteAppConfirmTitle: 'Delete this app?',
   deleteAppConfirmContent:
   deleteAppConfirmContent:
     'Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.',
     'Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.',

+ 4 - 0
web/i18n/zh-Hans/app.ts

@@ -13,6 +13,10 @@ const translation = {
   exportFailed: '导出 DSL 失败',
   exportFailed: '导出 DSL 失败',
   importDSL: '导入 DSL 文件',
   importDSL: '导入 DSL 文件',
   createFromConfigFile: '通过 DSL 文件创建',
   createFromConfigFile: '通过 DSL 文件创建',
+  importFromDSL: '导入 DSL',
+  importFromDSLFile: '文件',
+  importFromDSLUrl: 'URL',
+  importFromDSLUrlPlaceholder: '输入 DSL 文件的 URL',
   deleteAppConfirmTitle: '确认删除应用?',
   deleteAppConfirmTitle: '确认删除应用?',
   deleteAppConfirmContent:
   deleteAppConfirmContent:
     '删除应用将无法撤销。用户将不能访问你的应用,所有 Prompt 编排配置和日志均将一并被删除。',
     '删除应用将无法撤销。用户将不能访问你的应用,所有 Prompt 编排配置和日志均将一并被删除。',

+ 4 - 0
web/service/apps.ts

@@ -37,6 +37,10 @@ export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string
   return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon, icon_background } })
   return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon, icon_background } })
 }
 }
 
 
+export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
+  return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
+}
+
 export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon: string; icon_background: string }> = ({ appID, name, icon, icon_background }) => {
 export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon: string; icon_background: string }> = ({ appID, name, icon, icon_background }) => {
   return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon, icon_background } })
   return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon, icon_background } })
 }
 }