From 9a536979ab458381d7e82860459c413fa7fa6c51 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 15 Jul 2024 16:24:03 +0800 Subject: [PATCH] feat(frontend): workflow import dsl from url (#6286) --- web/app/(commonLayout)/apps/NewAppCard.tsx | 30 +++- .../app/create-from-dsl-modal/index.tsx | 133 +++++++++++++++--- .../billing/apps-full-in-dialog/index.tsx | 8 +- web/i18n/en-US/app.ts | 4 + web/i18n/zh-Hans/app.ts | 4 + web/service/apps.ts | 4 + 6 files changed, 158 insertions(+), 25 deletions(-) diff --git a/web/app/(commonLayout)/apps/NewAppCard.tsx b/web/app/(commonLayout)/apps/NewAppCard.tsx index 16f6ee26f0..c0dffa99ab 100644 --- a/web/app/(commonLayout)/apps/NewAppCard.tsx +++ b/web/app/(commonLayout)/apps/NewAppCard.tsx @@ -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(({ 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 ( (({ onSuc /> setShowCreateFromDSLModal(false)} + onClose={() => { + setShowCreateFromDSLModal(false) + + if (dslUrl) + replace('/') + }} + activeTab={activeTab} + dslUrl={dslUrl} onSuccess={() => { onPlanInfoChanged() if (onSuccess) diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index b0b617c470..0fc83f16b6 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -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() const [fileContent, setFileContent] = useState() + 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 ( { }} > -
{t('app.createFromConfigFile')}
-
- +
+ {t('app.importFromDSL')} +
onClose()} + > + +
- - {isAppsFull && } -
+
+ { + tabs.map(tab => ( +
setCurrentTab(tab.key)} + > + {tab.label} + { + currentTab === tab.key && ( +
+ ) + } +
+ )) + } +
+
+ { + currentTab === CreateFromDSLModalTab.FROM_FILE && ( + + ) + } + { + currentTab === CreateFromDSLModalTab.FROM_URL && ( +
+
DSL URL
+ setDslUrlValue(e.target.value)} + /> +
+ ) + } +
+ {isAppsFull && ( +
+ +
+ )} +
- +
) diff --git a/web/app/components/billing/apps-full-in-dialog/index.tsx b/web/app/components/billing/apps-full-in-dialog/index.tsx index 37abfebc50..b3601dbb10 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.tsx +++ b/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 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 ( -
+
{t('billing.apps.fullTipLine1')}
diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 6153c11873..1c70618a4f 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -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.', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 4008a247c9..002284b91b 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -13,6 +13,10 @@ const translation = { exportFailed: '导出 DSL 失败', importDSL: '导入 DSL 文件', createFromConfigFile: '通过 DSL 文件创建', + importFromDSL: '导入 DSL', + importFromDSLFile: '文件', + importFromDSLUrl: 'URL', + importFromDSLUrlPlaceholder: '输入 DSL 文件的 URL', deleteAppConfirmTitle: '确认删除应用?', deleteAppConfirmContent: '删除应用将无法撤销。用户将不能访问你的应用,所有 Prompt 编排配置和日志均将一并被删除。', diff --git a/web/service/apps.ts b/web/service/apps.ts index 1da792646f..5f2e9a3ffb 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -37,6 +37,10 @@ export const importApp: Fetcher('apps/import', { body: { data, name, description, icon, icon_background } }) } +export const importAppFromUrl: Fetcher = ({ url, name, description, icon, icon_background }) => { + return post('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 } }) }