From c052a69c4cba5e0e8fabd4823908e87c900bc35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A8=E6=AC=A3?= Date: Fri, 23 Aug 2024 00:47:04 +0800 Subject: [PATCH] feat: Add support for importing and exporting flows - Add new components for importing and exporting flows: ImportFlowModal and ExportFlowModal. - Update the necessary API functions in request.ts to handle flow import and export. - Update i18n.ts to include success messages for flow export and import. - Improve error handling and display appropriate messages in case of failures. --- web/app/i18n.ts | 4 + web/client/api/request.ts | 2 +- .../flow/canvas-modal/export-flow-modal.tsx | 90 +++++++++ .../flow/canvas-modal/import-flow-modal.tsx | 82 ++++++++ web/components/flow/canvas-modal/index.ts | 3 + .../flow/canvas-modal/save-flow-modal.tsx | 152 +++++++++++++++ web/components/flow/canvas-node.tsx | 2 +- web/pages/flow/canvas/index.tsx | 179 ++++-------------- web/types/flow.ts | 1 + 9 files changed, 366 insertions(+), 149 deletions(-) create mode 100644 web/components/flow/canvas-modal/export-flow-modal.tsx create mode 100644 web/components/flow/canvas-modal/import-flow-modal.tsx create mode 100644 web/components/flow/canvas-modal/index.ts create mode 100644 web/components/flow/canvas-modal/save-flow-modal.tsx diff --git a/web/app/i18n.ts b/web/app/i18n.ts index f4ae1c6bd..acb32e9ba 100644 --- a/web/app/i18n.ts +++ b/web/app/i18n.ts @@ -205,6 +205,8 @@ const en = { flow_name_required: 'Please enter the flow name', flow_description_required: 'Please enter the flow description', save_flow_success: 'Save flow success', + export_flow_success: 'Export flow success', + import_flow_success: 'Import flow success', delete_flow_confirm: 'Are you sure you want to delete this flow?', related_nodes: 'Related Nodes', add_resource: 'Add Resource', @@ -441,6 +443,8 @@ const zh: Resources['translation'] = { flow_name_required: '请输入工作流名称', flow_description_required: '请输入工作流描述', save_flow_success: '保存工作流成功', + export_flow_success: '导出工作流成功', + import_flow_success: '导入工作流成功', delete_flow_confirm: '确定删除该工作流吗?', related_nodes: '关联节点', language_select_tips: '请选择语言', diff --git a/web/client/api/request.ts b/web/client/api/request.ts index b0d705527..3f699654c 100644 --- a/web/client/api/request.ts +++ b/web/client/api/request.ts @@ -307,7 +307,7 @@ export const debugFlow = (data: any) => { }; export const exportFlow = (data: IFlowExportParams) => { - return GET('/api/v2/serve/awel/flow/export', data); + return GET(`/api/v2/serve/awel/flow/export/${data.uid}`, data); }; export const importFlow = (data: IFlowImportParams) => { diff --git a/web/components/flow/canvas-modal/export-flow-modal.tsx b/web/components/flow/canvas-modal/export-flow-modal.tsx new file mode 100644 index 000000000..486ebb987 --- /dev/null +++ b/web/components/flow/canvas-modal/export-flow-modal.tsx @@ -0,0 +1,90 @@ +import { Modal, Form, Input, Button, Space, Radio, message } from 'antd'; +import { IFlowData, IFlowUpdateParam } from '@/types/flow'; +import { apiInterceptors, exportFlow } from '@/client/api'; +import { ReactFlowInstance } from 'reactflow'; +import { useTranslation } from 'react-i18next'; + +type Props = { + reactFlow: ReactFlowInstance; + flowInfo?: IFlowUpdateParam; + isExportFlowModalOpen: boolean; + setIsExportFlowModalOpen: (value: boolean) => void; +}; + +export const ExportFlowModal: React.FC = ({ reactFlow, flowInfo, isExportFlowModalOpen, setIsExportFlowModalOpen }) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + const onFlowExport = async (values: any) => { + const flowData = reactFlow.toObject() as IFlowData; + const blob = new Blob([JSON.stringify(flowData)], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = values.file_name || 'flow.json'; + a.click(); + + const [, , res] = await apiInterceptors(exportFlow(values)); + + if (res?.success) { + messageApi.success(t('export_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + + setIsExportFlowModalOpen(false); + }; + + return ( + <> + setIsExportFlowModalOpen(false)} footer={null}> +
+ + + + + + + JSON + DBGPTS + + + + + + File + JSON + + + + + + + + + + + +
+
+ + {contextHolder} + + ); +}; diff --git a/web/components/flow/canvas-modal/import-flow-modal.tsx b/web/components/flow/canvas-modal/import-flow-modal.tsx new file mode 100644 index 000000000..fe2855aee --- /dev/null +++ b/web/components/flow/canvas-modal/import-flow-modal.tsx @@ -0,0 +1,82 @@ +import { Modal, Form, Button, Space, message, Checkbox, Upload } from 'antd'; +import { apiInterceptors, importFlow } from '@/client/api'; +import { Node, Edge } from 'reactflow'; +import { UploadOutlined } from '@mui/icons-material'; +import { t } from 'i18next'; + +type Props = { + isImportModalOpen: boolean; + setNodes: React.Dispatch[]>>; + setEdges: React.Dispatch[]>>; + setIsImportFlowModalOpen: (value: boolean) => void; +}; + +export const ImportFlowModal: React.FC = ({ setNodes, setEdges, isImportModalOpen, setIsImportFlowModalOpen }) => { + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + // TODO: Implement onFlowImport + const onFlowImport = async (values: any) => { + // const input = document.createElement('input'); + // input.type = 'file'; + // input.accept = '.json'; + // input.onchange = async (e: any) => { + // const file = e.target.files[0]; + // const reader = new FileReader(); + // reader.onload = async (event) => { + // const flowData = JSON.parse(event.target?.result as string) as IFlowData; + // setNodes(flowData.nodes); + // setEdges(flowData.edges); + // }; + // reader.readAsText(file); + // }; + // input.click; + console.log(values); + values.file = values.file?.[0]; + + const [, , res] = await apiInterceptors(importFlow(values)); + + if (res?.success) { + messageApi.success(t('export_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + + setIsImportFlowModalOpen(false); + }; + + return ( + <> + setIsImportFlowModalOpen(false)} footer={null}> +
+ (Array.isArray(e) ? e : e && e.fileList)} + rules={[{ required: true, message: 'Please upload a file' }]} + > + false} maxCount={1}> + + + + + + + + + + + + + + +
+
+ + {contextHolder} + + ); +}; diff --git a/web/components/flow/canvas-modal/index.ts b/web/components/flow/canvas-modal/index.ts new file mode 100644 index 000000000..e131e7b65 --- /dev/null +++ b/web/components/flow/canvas-modal/index.ts @@ -0,0 +1,3 @@ +export * from './save-flow-modal'; +export * from './export-flow-modal'; +export * from './import-flow-modal'; \ No newline at end of file diff --git a/web/components/flow/canvas-modal/save-flow-modal.tsx b/web/components/flow/canvas-modal/save-flow-modal.tsx new file mode 100644 index 000000000..e03e036d0 --- /dev/null +++ b/web/components/flow/canvas-modal/save-flow-modal.tsx @@ -0,0 +1,152 @@ +import { useState } from 'react'; +import { Modal, Form, Input, Button, Space, message, Checkbox } from 'antd'; +import { IFlowData, IFlowUpdateParam } from '@/types/flow'; +import { apiInterceptors, addFlow, updateFlowById } from '@/client/api'; +import { mapHumpToUnderline } from '@/utils/flow'; +import { useTranslation } from 'react-i18next'; +import { ReactFlowInstance } from 'reactflow'; +import { useSearchParams } from 'next/navigation'; + +const { TextArea } = Input; + +type Props = { + reactFlow: ReactFlowInstance; + flowInfo?: IFlowUpdateParam; + isSaveFlowModalOpen: boolean; + setIsSaveFlowModalOpen: (value: boolean) => void; +}; + +export const SaveFlowModal: React.FC = ({ reactFlow, isSaveFlowModalOpen, flowInfo, setIsSaveFlowModalOpen }) => { + const [deploy, setDeploy] = useState(true); + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const id = searchParams?.get('id') || ''; + const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + function onLabelChange(e: React.ChangeEvent) { + const label = e.target.value; + // replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -. + let result = label + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_-]/g, '') + .toLowerCase(); + result = result; + form.setFieldsValue({ name: result }); + } + + async function onSaveFlow() { + const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue(); + console.log(form.getFieldsValue()); + const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData); + + if (id) { + const [, , res] = await apiInterceptors(updateFlowById(id, { name, label, description, editable, uid: id, flow_data: reactFlowObject, state })); + + if (res?.success) { + messageApi.success(t('save_flow_success')); + } else if (res?.err_msg) { + messageApi.error(res?.err_msg); + } + } else { + const [_, res] = await apiInterceptors(addFlow({ name, label, description, editable, flow_data: reactFlowObject, state })); + if (res?.uid) { + messageApi.success(t('save_flow_success')); + const history = window.history; + history.pushState(null, '', `/flow/canvas?id=${res.uid}`); + } + } + setIsSaveFlowModalOpen(false); + } + + return ( + <> + { + setIsSaveFlowModalOpen(false); + }} + cancelButtonProps={{ className: 'hidden' }} + okButtonProps={{ className: 'hidden' }} + > +
+ + + + + ({ + validator(_, value) { + const regex = /^[a-zA-Z0-9_\-]+$/; + if (!regex.test(value)) { + return Promise.reject('Can only contain numbers, letters, underscores, and dashes'); + } + return Promise.resolve(); + }, + }), + ]} + > + + + + +