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

This commit is contained in:
zxhlyh 2024-07-15 16:24:03 +08:00 committed by GitHub
parent 46a5294d94
commit 9a536979ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 158 additions and 25 deletions

View File

@ -1,10 +1,14 @@
'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 CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
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 { 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 { t } = useTranslation()
const { onPlanInfoChanged } = useProviderContext()
const searchParams = useSearchParams()
const { replace } = useRouter()
const dslUrl = searchParams.get('remoteInstallUrl') || undefined
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = 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 (
<a
ref={ref}
@ -65,7 +80,14 @@ const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuc
/>
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => setShowCreateFromDSLModal(false)}
onClose={() => {
setShowCreateFromDSLModal(false)
if (dslUrl)
replace('/')
}}
activeTab={activeTab}
dslUrl={dslUrl}
onSuccess={() => {
onPlanInfoChanged()
if (onSuccess)

View File

@ -1,7 +1,7 @@
'use client'
import type { MouseEventHandler } from 'react'
import { useRef, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
@ -10,25 +10,38 @@ import Uploader from './uploader'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
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 { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
type CreateFromDSLModalProps = {
show: boolean
onSuccess?: () => 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 { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [currentFile, setDSLFile] = useState<File>()
const [fileContent, setFileContent] = useState<string>()
const [currentTab, setCurrentTab] = useState(activeTab)
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
const readFile = (file: File) => {
const reader = new FileReader()
@ -53,15 +66,26 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp
const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = async () => {
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
return
if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
return
if (isCreatingRef.current)
return
isCreatingRef.current = true
if (!currentFile)
return
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)
onSuccess()
if (onClose)
@ -76,24 +100,95 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp
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 (
<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}
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>
<Uploader
file={currentFile}
updateFile={handleFile}
/>
{isAppsFull && <AppsFull loc='app-create-dsl' />}
<div className='pt-6 flex justify-end'>
<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>
{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 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>
</Modal>
)

View File

@ -8,14 +8,18 @@ import s from './style.module.css'
import cn from '@/utils/classnames'
import GridMask from '@/app/components/base/grid-mask'
const AppsFull: FC<{ loc: string }> = ({
const AppsFull: FC<{ loc: string; className?: string }> = ({
loc,
className,
}) => {
const { t } = useTranslation()
return (
<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={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
<div>{t('billing.apps.fullTipLine1')}</div>

View File

@ -13,6 +13,10 @@ const translation = {
exportFailed: 'Export DSL failed.',
importDSL: 'Import 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?',
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.',

View File

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

View File

@ -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 } })
}
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 }) => {
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon, icon_background } })
}