feat(web): workflow import & export
This commit is contained in:
@@ -1,98 +1,203 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-28 14:08:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:20:40
|
||||
*/
|
||||
/**
|
||||
* UploadWorkflowModal 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, Select, Steps, Flex, Alert, Row, Col, Statistic, Input, Button } from 'antd';
|
||||
import { Form, Select, Steps, Flex, Alert, Input, Button, Result } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { UploadWorkflowModalData, UploadWorkflowModalRef } from '../types'
|
||||
import type { UploadWorkflowModalData, UploadData, UploadWorkflowModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import UploadFiles from '@/components/Upload/UploadFiles'
|
||||
import { fileUploadUrl } from '@/api/fileStorage'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { importWorkflow, completeImportWorkflow } from '@/api/application'
|
||||
|
||||
/**
|
||||
* Props for UploadWorkflowModal component
|
||||
*/
|
||||
interface UploadWorkflowModalProps {
|
||||
/** Function to refresh the parent component after workflow import */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps definition for the upload process
|
||||
*/
|
||||
const steps = [
|
||||
'upload',
|
||||
'complex',
|
||||
'node',
|
||||
'configCheck',
|
||||
'sureInfo',
|
||||
'completed'
|
||||
'upload', // Step 1: File upload
|
||||
'complex', // Step 2: Error/warning display
|
||||
'sureInfo', // Step 3: Information confirmation
|
||||
'completed' // Step 4: Success message
|
||||
]
|
||||
|
||||
/**
|
||||
* UploadWorkflowModal component
|
||||
*
|
||||
* @param {UploadWorkflowModalProps} props - Component props
|
||||
* @param {React.Ref<UploadWorkflowModalRef>} ref - Ref for imperative methods
|
||||
*/
|
||||
const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<UploadWorkflowModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [current, setCurrent] = useState<number>(5);
|
||||
|
||||
// State management
|
||||
const [visible, setVisible] = useState(false); // Modal visibility
|
||||
const [form] = Form.useForm<UploadWorkflowModalData>(); // Form instance
|
||||
const [loading, setLoading] = useState(false); // Loading state
|
||||
const [current, setCurrent] = useState<number>(0); // Current step
|
||||
const [data, setData] = useState<UploadData | null>(null); // Upload response data
|
||||
const [firstFormData, setFirstFormData] = useState<UploadWorkflowModalData | null>(null); // First step form data
|
||||
const [appId, setAppId] = useState<string | null>(null); // Imported application ID
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
/**
|
||||
* Handle modal close
|
||||
* Resets all states and form fields
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setData(null);
|
||||
setCurrent(0);
|
||||
setFirstFormData(null);
|
||||
setAppId(null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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:
|
||||
setCurrent(1)
|
||||
case 0: // Step 1: Upload file
|
||||
const formData = new FormData();
|
||||
setFirstFormData(values);
|
||||
formData.append('platform', values.platform);
|
||||
formData.append('file', values.file[0]);
|
||||
|
||||
// Call import workflow API
|
||||
importWorkflow(formData)
|
||||
.then(res => {
|
||||
const response = res as UploadData;
|
||||
const { errors, warnings } = response;
|
||||
setData(response);
|
||||
|
||||
// Navigate to error/warning step if any, otherwise go to confirmation
|
||||
if (errors.length || warnings.length) {
|
||||
setCurrent(1);
|
||||
} else {
|
||||
setCurrent(2);
|
||||
// Pre-fill form with file information
|
||||
form.setFieldsValue({
|
||||
name: values.file[0].name.split('.')[0],
|
||||
platform: values.platform,
|
||||
fileName: values.file[0].name,
|
||||
fileSize: values.file[0].size,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
setCurrent(2)
|
||||
case 1: // Step 2: Error/warning display
|
||||
if (firstFormData) {
|
||||
const { file, platform } = firstFormData;
|
||||
// Pre-fill form with file information
|
||||
form.setFieldsValue({
|
||||
name: file[0].name.split('.')[0],
|
||||
platform: platform,
|
||||
fileName: file[0].name,
|
||||
fileSize: file[0].size,
|
||||
});
|
||||
}
|
||||
setCurrent(2);
|
||||
break;
|
||||
case 2:
|
||||
setCurrent(3)
|
||||
break;
|
||||
case 3:
|
||||
setCurrent(4)
|
||||
break;
|
||||
case 4:
|
||||
setCurrent(5)
|
||||
break;
|
||||
case 5:
|
||||
case 2: // Step 3: Confirm information
|
||||
if (data) {
|
||||
// Complete import workflow
|
||||
completeImportWorkflow({
|
||||
temp_id: data.temp_id,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
})
|
||||
.then((res) => {
|
||||
const response = res as { id: string };
|
||||
setCurrent(3);
|
||||
setAppId(response.id);
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
setCurrent(prev => prev + 1)
|
||||
setCurrent(prev => prev + 1);
|
||||
break;
|
||||
}
|
||||
// form
|
||||
// .validateFields()
|
||||
// .then(() => {
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log('err', err)
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
/**
|
||||
* Handle navigation to previous step
|
||||
* Adjusts step based on whether there were errors/warnings
|
||||
*/
|
||||
const handleLastStep = () => {
|
||||
setCurrent(prev => prev - 1)
|
||||
}
|
||||
let newStep = current - 1;
|
||||
// If no errors or warnings, skip the error/warning step
|
||||
if (!data?.warnings?.length && !data?.errors?.length) {
|
||||
newStep = current - 2;
|
||||
}
|
||||
|
||||
// Reset form if not going back to error/warning step
|
||||
if (newStep !== 1) {
|
||||
form.resetFields();
|
||||
}
|
||||
setCurrent(newStep);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle navigation after successful import
|
||||
* @param {string} type - Navigation type ('detail' or 'list')
|
||||
*/
|
||||
const handleJump = (type: string) => {
|
||||
switch(type) {
|
||||
case 'detail':
|
||||
break;
|
||||
default:
|
||||
// Open application detail page in new tab
|
||||
window.open(`/#/application/config/${appId}`, '_blank');
|
||||
break;
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate modal footer based on current step
|
||||
*/
|
||||
const getFooter = useMemo(() => {
|
||||
switch(current) {
|
||||
case 0:
|
||||
case 0: // Step 1: Upload
|
||||
return [
|
||||
<Button key="back" onClick={handleClose}>
|
||||
{t('common.cancel')}
|
||||
@@ -103,30 +208,18 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
||||
loading={loading}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('application.nextStep')}
|
||||
{t('common.nextStep')}
|
||||
</Button>
|
||||
]
|
||||
case 5:
|
||||
return [
|
||||
<Button key="back" onClick={() => handleJump('list')}>
|
||||
{t('application.gotoList')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => handleJump('detail')}
|
||||
>
|
||||
{t('application.gotoDetail')}
|
||||
</Button>
|
||||
]
|
||||
default:
|
||||
];
|
||||
case 3: // Step 4: Completed
|
||||
return null;
|
||||
default: // Steps 1-2
|
||||
return [
|
||||
<Button onClick={handleClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>,
|
||||
<Button key="back" onClick={handleLastStep}>
|
||||
{t('application.lastStep')}
|
||||
{t('common.prevStep')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
@@ -134,11 +227,11 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
||||
loading={loading}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('application.nextStep')}
|
||||
{t('common.nextStep')}
|
||||
</Button>
|
||||
]
|
||||
];
|
||||
}
|
||||
}, [current])
|
||||
}, [current]);
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
@@ -150,6 +243,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
||||
footer={getFooter}
|
||||
width={1000}
|
||||
>
|
||||
{/* Steps indicator */}
|
||||
<div className='rb:p-3 rb:bg-[#FBFDFF] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:mb-3'>
|
||||
<Steps
|
||||
labelPlacement="vertical"
|
||||
@@ -158,25 +252,29 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
||||
items={steps.map(key => ({ title: t(`application.${key}`) }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 1: File upload */}
|
||||
{current === 0 &&
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
platform: 'dify'
|
||||
}}
|
||||
>
|
||||
<Form.Item name="provider" label={t('application.workflowProvider')}>
|
||||
<Form.Item name="platform" label={t('application.platform')}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ label: 'Dify', value: 'dify' },
|
||||
]}
|
||||
options={['dify'].map(value => ({
|
||||
label: t(`application.${value}`), value: value,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="file" valuePropName="fileList" noStyle>
|
||||
<UploadFiles
|
||||
action={fileUploadUrl}
|
||||
isAutoUpload={false}
|
||||
isCanDrag={true}
|
||||
fileSize={100}
|
||||
multiple={true}
|
||||
maxCount={1}
|
||||
fileType={['yml', 'yaml', 'zip', 'json']}
|
||||
onChange={(fileList) => {
|
||||
@@ -187,78 +285,76 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
|
||||
</Form>
|
||||
}
|
||||
|
||||
{/* Step 2: Error/warning display */}
|
||||
{current === 1 &&
|
||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
||||
{['fileType', 'parse', 'nodes', 'variable'].map(key => (
|
||||
<Alert key={key} message={key} type="success" showIcon />
|
||||
{data?.warnings.map(vo => (
|
||||
<Alert
|
||||
key={vo.node_id}
|
||||
message={<div>
|
||||
<div>{vo.node_name || vo.node_id} - {vo.type}</div>
|
||||
{vo.detail}
|
||||
</div>}
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
))}
|
||||
{data?.errors.map(vo => (
|
||||
<Alert
|
||||
key={vo.node_id}
|
||||
message={<div>
|
||||
<div>{vo.node_name || vo.node_id} - {vo.type}</div>
|
||||
{vo.detail}
|
||||
</div>}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
))}
|
||||
|
||||
<Row gutter={12}>
|
||||
{['complex', 'nodes', 'task'].map(key => (
|
||||
<Col key={key} span={8}>
|
||||
<Statistic title={key} value={0} className="rb:text-center rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3!" />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Flex>
|
||||
}
|
||||
|
||||
{/* 节点映射 */}
|
||||
{/* Step 3: Information confirmation */}
|
||||
{current === 2 &&
|
||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
||||
<RbCard>
|
||||
<Flex justify="space-around">
|
||||
<div> Left Node</div>
|
||||
→
|
||||
<div>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
className="rb:w-50"
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</Flex>
|
||||
}
|
||||
{current === 3 &&
|
||||
<Flex vertical gap={12} className="rb:w-[70%]! rb:mx-auto!">
|
||||
|
||||
</Flex>
|
||||
}
|
||||
{current === 4 &&
|
||||
<Form form={form} layout="horizontal" className="rb:w-[70%]! rb:mx-auto!">
|
||||
<Form form={form} layout="vertical" className="rb:w-[70%]! rb:mx-auto!">
|
||||
<div className="rb:text-[#5B6167] rb:font-medium">{t('application.baseInfo')}</div>
|
||||
<Form.Item name="name" label={t('application.workflowName')} rules={[{ required: true }]}>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="source" label={t('application.source')}>
|
||||
source
|
||||
<Form.Item name="platform" label={t('application.platform')}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="fileName" label={t('application.fileName')}>
|
||||
fileName
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="fileSize" label={t('application.fileSize')}>
|
||||
fileSize
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="desciption" label={t('application.desciption')}>
|
||||
<Form.Item name="description" label={t('application.description')} layout="vertical">
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:font-medium">{t('application.importStatistic')}</div>
|
||||
<Row gutter={12}>
|
||||
{['complex', 'nodes', 'task'].map(key => (
|
||||
<Col key={key} span={8}>
|
||||
<Statistic title={key} value={0} className="rb:text-center rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3!" />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Form>
|
||||
}
|
||||
{current === 5 &&
|
||||
<Flex justify="center" vertical gap={12} className="rb:w-[70%]! rb:mx-auto! rb:text-center">
|
||||
<div>导入成功</div>
|
||||
<div>您的工作流已成功导入,可以在应用管理中查看和管理</div>
|
||||
</Flex>
|
||||
|
||||
{/* Step 4: Success message */}
|
||||
{current === 3 &&
|
||||
<Result
|
||||
status="success"
|
||||
title={t('application.importSuccess')}
|
||||
subTitle={t('application.importSuccessDesc')}
|
||||
extra={[
|
||||
<Button key="back" onClick={() => handleJump('list')}>
|
||||
{t('application.gotoList')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => handleJump('detail')}
|
||||
>
|
||||
{t('application.gotoDetail')}
|
||||
</Button>
|
||||
]}
|
||||
/>
|
||||
}
|
||||
</RbModal>
|
||||
);
|
||||
|
||||
@@ -83,9 +83,9 @@ const ApplicationManagement: React.FC = () => {
|
||||
setQuery(prev => ({...prev, type: value}))
|
||||
}
|
||||
|
||||
// const handleImport = () => {
|
||||
// uploadWorkflowModalRef.current?.handleOpen()
|
||||
// }
|
||||
const handleImport = () => {
|
||||
uploadWorkflowModalRef.current?.handleOpen()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
@@ -111,9 +111,9 @@ const ApplicationManagement: React.FC = () => {
|
||||
</Col>
|
||||
<Col span={12} className="rb:text-right">
|
||||
<Space size={12}>
|
||||
{/* <Button onClick={handleImport}>
|
||||
<Button onClick={handleImport}>
|
||||
{t('application.importWorkflow')}
|
||||
</Button> */}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleCreate}>
|
||||
{t('application.createApplication')}
|
||||
</Button>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:34:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-06 11:08:37
|
||||
* @Last Modified time: 2026-02-28 16:16:03
|
||||
*/
|
||||
/**
|
||||
* Type definitions for Application Management
|
||||
*/
|
||||
|
||||
import type { WorkflowConfig } from '@/views/Workflow/types';
|
||||
/**
|
||||
* Search query parameters
|
||||
*/
|
||||
@@ -174,9 +174,63 @@ export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Upload workflow modal form data
|
||||
*/
|
||||
export interface UploadWorkflowModalData {
|
||||
/** Platform type (e.g., 'dify') */
|
||||
platform: string;
|
||||
/** Array of uploaded files */
|
||||
file: any[];
|
||||
/** Optional workflow name */
|
||||
name?: string;
|
||||
/** Optional original file name */
|
||||
fileName?: string;
|
||||
/** Optional file size in bytes */
|
||||
fileSize?: number;
|
||||
/** Optional workflow description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex item for errors and warnings
|
||||
*/
|
||||
interface ComplexItem {
|
||||
/** Error/warning type */
|
||||
type: string;
|
||||
/** Detailed error/warning message */
|
||||
detail: string;
|
||||
/** Node identifier where the error/warning occurred */
|
||||
node_id: string;
|
||||
/** Node name where the error/warning occurred */
|
||||
node_name: string;
|
||||
/** Optional scope of the error/warning */
|
||||
scope: string | null;
|
||||
/** Optional name associated with the error/warning */
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload data response
|
||||
* @extends WorkflowConfig
|
||||
*/
|
||||
export interface UploadData extends WorkflowConfig {
|
||||
/** Whether the upload was successful */
|
||||
success: boolean;
|
||||
/** Temporary identifier for the uploaded workflow */
|
||||
temp_id: string;
|
||||
/** Optional workflow identifier if already exists */
|
||||
workflow_id?: string;
|
||||
/** Array of error items */
|
||||
errors: ComplexItem[];
|
||||
/** Array of warning items */
|
||||
warnings: ComplexItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload workflow modal ref interface
|
||||
*/
|
||||
export interface UploadWorkflowModalRef {
|
||||
/** Open the upload workflow modal */
|
||||
handleOpen: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user