mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-11 13:58:58 +00:00
feat: add support for importing and exporting flows (#1869)
# 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.
This commit is contained in:
@@ -205,6 +205,8 @@ const en = {
|
|||||||
flow_name_required: 'Please enter the flow name',
|
flow_name_required: 'Please enter the flow name',
|
||||||
flow_description_required: 'Please enter the flow description',
|
flow_description_required: 'Please enter the flow description',
|
||||||
save_flow_success: 'Save flow success',
|
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?',
|
delete_flow_confirm: 'Are you sure you want to delete this flow?',
|
||||||
related_nodes: 'Related Nodes',
|
related_nodes: 'Related Nodes',
|
||||||
add_resource: 'Add Resource',
|
add_resource: 'Add Resource',
|
||||||
@@ -441,6 +443,8 @@ const zh: Resources['translation'] = {
|
|||||||
flow_name_required: '请输入工作流名称',
|
flow_name_required: '请输入工作流名称',
|
||||||
flow_description_required: '请输入工作流描述',
|
flow_description_required: '请输入工作流描述',
|
||||||
save_flow_success: '保存工作流成功',
|
save_flow_success: '保存工作流成功',
|
||||||
|
export_flow_success: '导出工作流成功',
|
||||||
|
import_flow_success: '导入工作流成功',
|
||||||
delete_flow_confirm: '确定删除该工作流吗?',
|
delete_flow_confirm: '确定删除该工作流吗?',
|
||||||
related_nodes: '关联节点',
|
related_nodes: '关联节点',
|
||||||
language_select_tips: '请选择语言',
|
language_select_tips: '请选择语言',
|
||||||
|
@@ -307,7 +307,7 @@ export const debugFlow = (data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const exportFlow = (data: IFlowExportParams) => {
|
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) => {
|
export const importFlow = (data: IFlowImportParams) => {
|
||||||
|
90
web/components/flow/canvas-modal/export-flow-modal.tsx
Normal file
90
web/components/flow/canvas-modal/export-flow-modal.tsx
Normal file
@@ -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
web/components/flow/canvas-modal/import-flow-modal.tsx
Normal file
82
web/components/flow/canvas-modal/import-flow-modal.tsx
Normal file
@@ -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
web/components/flow/canvas-modal/index.ts
Normal file
3
web/components/flow/canvas-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './save-flow-modal';
|
||||||
|
export * from './export-flow-modal';
|
||||||
|
export * from './import-flow-modal';
|
152
web/components/flow/canvas-modal/save-flow-modal.tsx
Normal file
152
web/components/flow/canvas-modal/save-flow-modal.tsx
Normal file
@@ -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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,4 +1,4 @@
|
|||||||
import { IFlowNode, IFlowRefreshParams } from '@/types/flow';
|
import { IFlowNode } from '@/types/flow';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import NodeParamHandler from './node-param-handler';
|
import NodeParamHandler from './node-param-handler';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
import { addFlow, apiInterceptors, getFlowById, updateFlowById } from '@/client/api';
|
import { apiInterceptors, getFlowById, importFlow } from '@/client/api';
|
||||||
import MuiLoading from '@/components/common/loading';
|
import MuiLoading from '@/components/common/loading';
|
||||||
import AddNodes from '@/components/flow/add-nodes';
|
import AddNodes from '@/components/flow/add-nodes';
|
||||||
import ButtonEdge from '@/components/flow/button-edge';
|
import ButtonEdge from '@/components/flow/button-edge';
|
||||||
import CanvasNode from '@/components/flow/canvas-node';
|
import CanvasNode from '@/components/flow/canvas-node';
|
||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
|
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
|
||||||
import { ExportOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExportOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { Button, Checkbox, Divider, Form, Input, Modal, Space, Tooltip, message, notification } from 'antd';
|
import { Divider, Space, Tooltip, message, notification } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
|
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
|
import { SaveFlowModal, ExportFlowModal, ImportFlowModal } from '@/components/flow/canvas-modal';
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// Define your component props here
|
// Define your component props here
|
||||||
@@ -23,8 +22,7 @@ const edgeTypes = { buttonedge: ButtonEdge };
|
|||||||
|
|
||||||
const Canvas: React.FC<Props> = () => {
|
const Canvas: React.FC<Props> = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
|
||||||
const [form] = Form.useForm<IFlowUpdateParam>();
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const id = searchParams?.get('id') || '';
|
const id = searchParams?.get('id') || '';
|
||||||
const reactFlow = useReactFlow();
|
const reactFlow = useReactFlow();
|
||||||
@@ -33,9 +31,10 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
||||||
const [deploy, setDeploy] = useState(true);
|
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
||||||
|
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
||||||
|
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
|
||||||
|
|
||||||
async function getFlowData() {
|
async function getFlowData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -139,17 +138,6 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
event.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function labelChange(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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSave() {
|
function onSave() {
|
||||||
const flowData = reactFlow.toObject() as IFlowData;
|
const flowData = reactFlow.toObject() as IFlowData;
|
||||||
const [check, node, message] = checkFlowDataRequied(flowData);
|
const [check, node, message] = checkFlowDataRequied(flowData);
|
||||||
@@ -172,60 +160,16 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
);
|
);
|
||||||
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
|
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
|
||||||
}
|
}
|
||||||
setIsModalVisible(true);
|
setIsSaveFlowModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: EXport flow data
|
|
||||||
function onExport() {
|
function onExport() {
|
||||||
const flowData = reactFlow.toObject() as IFlowData;
|
setIsExportFlowModalOpen(true);
|
||||||
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 = 'flow.json';
|
|
||||||
a.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Import flow data
|
// TODO: Import flow data
|
||||||
function onImport() {
|
function onImport() {
|
||||||
const input = document.createElement('input');
|
setIsImportFlowModalOpen(true);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveFlow() {
|
|
||||||
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 }));
|
|
||||||
setIsModalVisible(false);
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
setIsModalVisible(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -269,86 +213,27 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
<AddNodes />
|
<AddNodes />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<SaveFlowModal
|
||||||
title={t('flow_modal_title')}
|
reactFlow={reactFlow}
|
||||||
open={isModalVisible}
|
flowInfo={flowInfo}
|
||||||
onCancel={() => {
|
isSaveFlowModalOpen={isSaveFlowModalOpen}
|
||||||
setIsModalVisible(false);
|
setIsSaveFlowModalOpen={setIsSaveFlowModalOpen}
|
||||||
}}
|
/>
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
|
||||||
okButtonProps={{ className: 'hidden' }}
|
<ExportFlowModal
|
||||||
>
|
reactFlow={reactFlow}
|
||||||
<Form
|
flowInfo={flowInfo}
|
||||||
name="flow_form"
|
isExportFlowModalOpen={isExportFlowModalOpen}
|
||||||
form={form}
|
setIsExportFlowModalOpen={setIsExportFlowModalOpen}
|
||||||
labelCol={{ span: 8 }}
|
/>
|
||||||
wrapperCol={{ span: 16 }}
|
|
||||||
style={{ maxWidth: 600 }}
|
<ImportFlowModal
|
||||||
initialValues={{ remember: true }}
|
setNodes={setNodes}
|
||||||
onFinish={handleSaveFlow}
|
setEdges={setEdges}
|
||||||
autoComplete="off"
|
isImportModalOpen={isImportModalOpen}
|
||||||
>
|
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
|
||||||
<Form.Item label="Title" name="label" initialValue={flowInfo?.label} rules={[{ required: true, message: 'Please input flow title!' }]}>
|
/>
|
||||||
<Input onChange={labelChange} />
|
|
||||||
</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'}
|
|
||||||
value={deploy}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.checked;
|
|
||||||
form.setFieldValue('state', val ? 'deployed' : 'developing');
|
|
||||||
setDeploy(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
htmlType="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsModalVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
{contextHolder}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -172,6 +172,7 @@ export type IFlowData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IFlowExportParams = {
|
export type IFlowExportParams = {
|
||||||
|
uid: string;
|
||||||
export_type?: 'json' | 'dbgpts';
|
export_type?: 'json' | 'dbgpts';
|
||||||
format?: 'json' | 'file';
|
format?: 'json' | 'file';
|
||||||
file_name?: string;
|
file_name?: string;
|
||||||
|
Reference in New Issue
Block a user