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' '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)

View File

@ -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({ let app
data: fileContent || '',
}) 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='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}> {t('app.importFromDSL')}
<RiCloseLine className='w-4 h-4 text-gray-500' /> <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>
<Uploader <div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
file={currentFile} {
updateFile={handleFile} tabs.map(tab => (
/> <div
{isAppsFull && <AppsFull loc='app-create-dsl' />} key={tab.key}
<div className='pt-6 flex justify-end'> 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 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>
) )

View File

@ -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>

View File

@ -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.',

View File

@ -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 编排配置和日志均将一并被删除。',

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 } }) 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 } })
} }