From 16e2c959651b5d43ff8ec48ee7093916978d8ec3 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 10 Mar 2026 14:22:15 +0800 Subject: [PATCH] feat(web): app add export & import --- web/src/api/application.ts | 8 + web/src/utils/request.ts | 17 ++ .../views/ApplicationConfig/ReleasePage.tsx | 7 +- .../components/ConfigHeader.tsx | 11 +- .../components/UploadModal.tsx | 256 ++++++++++++++++++ web/src/views/ApplicationManagement/index.tsx | 9 + web/src/views/ApplicationManagement/types.ts | 8 + 7 files changed, 308 insertions(+), 8 deletions(-) create mode 100644 web/src/views/ApplicationManagement/components/UploadModal.tsx diff --git a/web/src/api/application.ts b/web/src/api/application.ts index c769dd91..71048454 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -135,4 +135,12 @@ export const getExperienceConfig = (share_token: string) => { 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}` } }) +} +// Export application +export const appExport = (app_id: string, appName: string, data?: { release_version: string }) => { + return request.getDownloadFile(`/apps/${app_id}/export`, `${appName}.yml`, data) +} +// Import application +export const appImport = (formData: FormData) => { + return request.uploadFile(`/apps/import`, formData) } \ No newline at end of file diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 3f81d4ab..80c12f85 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -347,6 +347,23 @@ export const request = { document.body.removeChild(link); callback?.() }); + }, + getDownloadFile(url: string, fileName: string, data?: unknown, callback?: () => void) { + service.get(url, { + params: paramFilter(data as Record), + responseType: "blob", + }) + .then(res => { + const link = document.createElement("a"); + const blob = new Blob([res as unknown as BlobPart]); + link.style.display = "none"; + link.href = URL.createObjectURL(blob); + link.setAttribute("download", decodeURI(fileName || fileName)); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + callback?.() + }); } }; diff --git a/web/src/views/ApplicationConfig/ReleasePage.tsx b/web/src/views/ApplicationConfig/ReleasePage.tsx index 63f6df71..b44252d6 100644 --- a/web/src/views/ApplicationConfig/ReleasePage.tsx +++ b/web/src/views/ApplicationConfig/ReleasePage.tsx @@ -11,7 +11,7 @@ import { Button, Space, Input, Form, App } from 'antd'; import Tag, { type TagProps } from './components/Tag' import RbCard from '@/components/RbCard/Card' -import { getReleaseList, rollbackRelease } from '@/api/application' +import { getReleaseList, rollbackRelease, appExport } from '@/api/application' import ReleaseModal from './components/ReleaseModal' import ReleaseShareModal from './components/ReleaseShareModal' import type { Release, ReleaseModalRef, ReleaseShareModalRef } from './types' @@ -67,6 +67,9 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres message.success(t('common.operateSuccess')) }) } + const handleExport = () => { + appExport(data.id, data.name) + } return (
@@ -123,7 +126,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres {selectedVersion && <> - {/* */} + {data?.type !== 'multi_agent' && } {data.current_release_id !== selectedVersion.id && } } diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 42031d85..8131a819 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -19,9 +19,8 @@ import deleteIcon from '@/assets/images/delete_hover.svg' import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types'; import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal' import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types' -import { deleteApplication } from '@/api/application' +import { deleteApplication, appExport } from '@/api/application' import CopyModal from './CopyModal' -import { exportToYaml } from '@/utils/yamlExport'; const { Header } = Layout; @@ -85,16 +84,16 @@ const ConfigHeader: FC = ({ * Handle menu item click */ const handleClick: MenuProps['onClick'] = ({ key }) => { + if (!application) return switch (key) { case 'edit': - applicationModalRef.current?.handleOpen(application as Application) + applicationModalRef.current?.handleOpen(application) break; case 'copy': copyModalRef.current?.handleOpen() break; case 'export': - console.log('export', workflowRef?.current?.config) - exportToYaml(workflowRef?.current?.config, application?.name ?`${application?.name}.yml`: undefined) + appExport(application.id, application.name) break; case 'delete': handleDelete() @@ -153,7 +152,7 @@ const ConfigHeader: FC = ({ * Format dropdown menu items */ const formatMenuItems = useMemo(() => { - const items = (application?.type === 'workflow' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({ + const items = (application?.type !== 'multi_agent' ? ['edit', 'copy', 'export', 'delete'] : ['edit', 'copy', 'delete']).map(key => ({ key, icon: , label: t(`common.${key}`), diff --git a/web/src/views/ApplicationManagement/components/UploadModal.tsx b/web/src/views/ApplicationManagement/components/UploadModal.tsx new file mode 100644 index 00000000..f354c145 --- /dev/null +++ b/web/src/views/ApplicationManagement/components/UploadModal.tsx @@ -0,0 +1,256 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-28 14:08:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-06 12:05:46 + */ +/** + * UploadModal Component + * + * This component provides a modal for uploading workflow files with a multi-step process: + * 1. Upload - Select platform and file + * 2. Complex - Show warnings and errors if any + * 3. SureInfo - Confirm and edit workflow information + * 4. Completed - Show success message and options + */ +import { forwardRef, useImperativeHandle, useState, useMemo } from 'react'; +import { Form, Steps, Flex, Alert, Button, Result, message } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { Application, UploadModalRef } from '../types' +import RbModal from '@/components/RbModal' +import UploadFiles from '@/components/Upload/UploadFiles' +import { appImport } from '@/api/application' + +/** + * Props for UploadModal component + */ +interface UploadModalProps { + /** Function to refresh the parent component after workflow import */ + refresh: () => void; +} + + +/** + * Steps definition for the upload process + */ +const steps = [ + 'upload', // Step 1: File upload + 'complex', // Step 2: Error/warning display + 'completed' // Step 4: Success message +] +/** + * UploadModal component + * + * @param {UploadModalProps} props - Component props + * @param {React.Ref} ref - Ref for imperative methods + */ +const UploadModal = forwardRef(({ + refresh +}, ref) => { + const { t } = useTranslation(); + + // State management + const [visible, setVisible] = useState(false); // Modal visibility + const [form] = Form.useForm<{ file: File[] }>(); // Form instance + const [loading, setLoading] = useState(false); // Loading state + const [current, setCurrent] = useState(0); // Current step + const [appId, setAppId] = useState(null); // Imported application ID + const [warnings, setWarnings] = useState([]) + + /** + * Handle modal close + * Resets all states and form fields + */ + const handleClose = () => { + setVisible(false); + form.resetFields(); + setCurrent(0); + setAppId(null); + setLoading(false); + setWarnings([]) + }; + + /** + * Handle modal open + * Resets form fields and shows modal + */ + const handleOpen = () => { + form.resetFields(); + setVisible(true); + }; + + /** + * Handle save/submit action + * Processes different logic based on current step + */ + const handleSave = () => { + const values = form.getFieldsValue(); + + switch(current) { + case 0: // Step 1: Upload file + if (!values.file || values.file.length === 0) { + message.warning(t('application.pleaseUploadFile')); + return; + } + const formData = new FormData(); + formData.append('file', values.file[0]); + + setLoading(true) + // Call import API + appImport(formData) + .then(res => { + const { warnings, app } = res as { warnings: string[]; app: Application }; + + setAppId(app?.id) + if (warnings.length) { + setCurrent(1) + setWarnings(warnings) + } else { + setCurrent(2) + } + }) + .finally(() => setLoading(false)); + break; + case 2: + break; + } + }; + + // Expose methods to parent component via ref + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + /** + * Handle navigation after successful import + * @param {string} type - Navigation type ('detail' or 'list') + */ + const handleJump = (type: string) => { + handleClose(); + refresh(); + setTimeout(() => { + switch (type) { + case 'detail': + // Open application detail page in new tab + window.open(`/#/application/config/${appId}`, '_blank'); + break; + } + }, 100) + }; + + /** + * Generate modal footer based on current step + */ + const getFooter = useMemo(() => { + switch (current) { + case 0: // Step 1: Upload + return [ + , + + ]; + case 1: + return [ + , + + ] + default: + return null; + } + }, [current, loading]); + return ( + + {/* Steps indicator */} +
+ ({ title: t(`application.${key}`) }))} + /> +
+ {current === 0 && +
+ + + +
+ } + {/* Step 2: Error/warning display */} + {current === 1 && + + {warnings.map((vo, index) => ( + {vo}
} + type="warning" + showIcon + /> + ))} + + } + {current === 2 && + handleJump('list')}> + {t('application.gotoList')} + , + + ]} + /> + } + + ); +}); + +export default UploadModal; \ No newline at end of file diff --git a/web/src/views/ApplicationManagement/index.tsx b/web/src/views/ApplicationManagement/index.tsx index 055c0c8f..62c02574 100644 --- a/web/src/views/ApplicationManagement/index.tsx +++ b/web/src/views/ApplicationManagement/index.tsx @@ -25,6 +25,7 @@ import { getApplicationListUrl, deleteApplication } from '@/api/application' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import { formatDateTime } from '@/utils/format'; import UploadWorkflowModal from './components/UploadWorkflowModal' +import UploadModal from './components/UploadModal' /** * Application management main component @@ -37,6 +38,7 @@ const ApplicationManagement: React.FC = () => { const applicationModalRef = useRef(null); const scrollListRef = useRef(null) const uploadWorkflowModalRef = useRef(null); + const uploadModalRef = useRef(null); useEffect(() => { // Convert URLSearchParams to a plain object for easier access @@ -91,6 +93,8 @@ const ApplicationManagement: React.FC = () => { case 'thirdParty': handleImport() break; + case 'import': + uploadModalRef.current?.handleOpen() } } return ( @@ -121,6 +125,7 @@ const ApplicationManagement: React.FC = () => { @@ -186,6 +191,10 @@ const ApplicationManagement: React.FC = () => { ref={uploadWorkflowModalRef} refresh={refresh} /> + ); }; diff --git a/web/src/views/ApplicationManagement/types.ts b/web/src/views/ApplicationManagement/types.ts index 696b828a..f9e17c17 100644 --- a/web/src/views/ApplicationManagement/types.ts +++ b/web/src/views/ApplicationManagement/types.ts @@ -233,4 +233,12 @@ export interface UploadData extends WorkflowConfig { export interface UploadWorkflowModalRef { /** Open the upload workflow modal */ handleOpen: () => void; +} + +/** + * Upload app modal ref interface + */ +export interface UploadModalRef { + /** Open the upload workflow modal */ + handleOpen: () => void; } \ No newline at end of file