-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
9 changed files
with
366 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.