Skip to content

Commit

Permalink
feat: add support for importing and exporting flows (#1869)
Browse files Browse the repository at this point in the history
# Description

- 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.
  • Loading branch information
Dreammy23 authored Aug 22, 2024
2 parents 4848647 + c052a69 commit bef46d8
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 149 deletions.
4 changes: 4 additions & 0 deletions web/app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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: '请选择语言',
Expand Down
2 changes: 1 addition & 1 deletion web/client/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export const debugFlow = (data: any) => {
};

export const exportFlow = (data: IFlowExportParams) => {
return GET<IFlowExportParams, any>('/api/v2/serve/awel/flow/export', data);
return GET<IFlowExportParams, any>(`/api/v2/serve/awel/flow/export/${data.uid}`, data);
};

export const importFlow = (data: IFlowImportParams) => {
Expand Down
90 changes: 90 additions & 0 deletions web/components/flow/canvas-modal/export-flow-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<any, any>;
flowInfo?: IFlowUpdateParam;
isExportFlowModalOpen: boolean;
setIsExportFlowModalOpen: (value: boolean) => void;
};

export const ExportFlowModal: React.FC<Props> = ({ 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 (
<>
<Modal title="Export Flow" open={isExportFlowModalOpen} onCancel={() => setIsExportFlowModalOpen(false)} footer={null}>
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
initialValues={{
export_type: 'json',
format: 'file',
file_name: 'flow.json',
uid: flowInfo?.uid,
}}
onFinish={onFlowExport}
>
<Form.Item label="File Name" name="file_name" rules={[{ required: true, message: 'Please input file name!' }]}>
<Input placeholder="file.json" />
</Form.Item>

<Form.Item label="Export Type" name="export_type">
<Radio.Group>
<Radio value="json">JSON</Radio>
<Radio value="dbgpts">DBGPTS</Radio>
</Radio.Group>
</Form.Item>

<Form.Item label="Format" name="format">
<Radio.Group>
<Radio value="file">File</Radio>
<Radio value="json">JSON</Radio>
</Radio.Group>
</Form.Item>

<Form.Item hidden name="uid">
<Input />
</Form.Item>

<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button onClick={() => setIsExportFlowModalOpen(false)}>Cancel</Button>
<Button type="primary" htmlType="submit">
Export
</Button>
</Space>
</Form.Item>
</Form>
</Modal>

{contextHolder}
</>
);
};
82 changes: 82 additions & 0 deletions web/components/flow/canvas-modal/import-flow-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<Node<any, string | undefined>[]>>;
setEdges: React.Dispatch<React.SetStateAction<Edge<any>[]>>;
setIsImportFlowModalOpen: (value: boolean) => void;
};

export const ImportFlowModal: React.FC<Props> = ({ 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 (
<>
<Modal title="Import Flow" open={isImportModalOpen} onCancel={() => setIsImportFlowModalOpen(false)} footer={null}>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} onFinish={onFlowImport}>
<Form.Item
name="file"
label="File"
valuePropName="fileList"
getValueFromEvent={(e) => (Array.isArray(e) ? e : e && e.fileList)}
rules={[{ required: true, message: 'Please upload a file' }]}
>
<Upload accept=".json,.zip" beforeUpload={() => false} maxCount={1}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
</Form.Item>

<Form.Item label="save flow" name="save_flow" valuePropName="checked">
<Checkbox />
</Form.Item>

<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button onClick={() => setIsImportFlowModalOpen(false)}>Cancel</Button>
<Button type="primary" htmlType="submit">
Import
</Button>
</Space>
</Form.Item>
</Form>
</Modal>

{contextHolder}
</>
);
};
3 changes: 3 additions & 0 deletions web/components/flow/canvas-modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './save-flow-modal';
export * from './export-flow-modal';
export * from './import-flow-modal';
152 changes: 152 additions & 0 deletions web/components/flow/canvas-modal/save-flow-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<any, any>;
flowInfo?: IFlowUpdateParam;
isSaveFlowModalOpen: boolean;
setIsSaveFlowModalOpen: (value: boolean) => void;
};

export const SaveFlowModal: React.FC<Props> = ({ reactFlow, isSaveFlowModalOpen, flowInfo, setIsSaveFlowModalOpen }) => {
const [deploy, setDeploy] = useState(true);
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const [form] = Form.useForm<IFlowUpdateParam>();
const [messageApi, contextHolder] = message.useMessage();

function onLabelChange(e: React.ChangeEvent<HTMLInputElement>) {
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 (
<>
<Modal
title={t('flow_modal_title')}
open={isSaveFlowModalOpen}
onCancel={() => {
setIsSaveFlowModalOpen(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name="flow_form"
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={onSaveFlow}
autoComplete="off"
>
<Form.Item label="Title" name="label" initialValue={flowInfo?.label} rules={[{ required: true, message: 'Please input flow title!' }]}>
<Input onChange={onLabelChange} />
</Form.Item>

<Form.Item
label="Name"
name="name"
initialValue={flowInfo?.name}
rules={[
{ required: true, message: 'Please input flow name!' },
() => ({
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();
},
}),
]}
>
<Input />
</Form.Item>

<Form.Item label="Description" initialValue={flowInfo?.description} name="description">
<TextArea rows={3} />
</Form.Item>

<Form.Item label="Editable" name="editable" initialValue={flowInfo?.editable} valuePropName="checked">
<Checkbox />
</Form.Item>

<Form.Item hidden name="state">
<Input />
</Form.Item>

<Form.Item label="Deploy">
<Checkbox
defaultChecked={flowInfo?.state === 'deployed' || flowInfo?.state === 'running'}
checked={deploy}
onChange={(e) => {
const val = e.target.checked;
form.setFieldValue('state', val ? 'deployed' : 'developing');
setDeploy(val);
}}
/>
</Form.Item>

<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button
htmlType="button"
onClick={() => {
setIsSaveFlowModalOpen(false);
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Form.Item>
</Form>
</Modal>

{contextHolder}
</>
);
};
2 changes: 1 addition & 1 deletion web/components/flow/canvas-node.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IFlowNode, IFlowRefreshParams } from '@/types/flow';
import { IFlowNode } from '@/types/flow';
import Image from 'next/image';
import NodeParamHandler from './node-param-handler';
import classNames from 'classnames';
Expand Down
Loading

0 comments on commit bef46d8

Please sign in to comment.