Merge branch 'feat/sprint-web-flow' into dev-0.6

This commit is contained in:
谨欣
2024-08-23 01:24:43 +08:00
35 changed files with 7868 additions and 266 deletions

View File

@@ -332,6 +332,17 @@ class Config(metaclass=Singleton):
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
)
# file server configuration
# The host of the current file server, if None, get the host automatically
self.FILE_SERVER_HOST = os.getenv("FILE_SERVER_HOST")
self.FILE_SERVER_LOCAL_STORAGE_PATH = os.getenv(
"FILE_SERVER_LOCAL_STORAGE_PATH"
)
# multi-instance flag
self.WEBSERVER_MULTI_INSTANCE = (
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
)
@property
def local_db_manager(self) -> "ConnectorManager":
from dbgpt.datasource.manages import ConnectorManager

View File

@@ -173,7 +173,6 @@ class _VariablesRequestBase(BaseModel):
description="The key of the variable to create",
examples=["dbgpt.model.openai.api_key"],
)
label: str = Field(
...,
description="The label of the variable to create",

View File

@@ -50,7 +50,7 @@ yarn install
### Usage
```sh
cp .env.example .env
cp .env.template .env
```
edit the `API_BASE_URL` to the real address

View File

@@ -1,3 +1,4 @@
import { Domain } from '@mui/icons-material';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from '@/locales/en';

View File

@@ -45,7 +45,19 @@ import {
import { BaseModelParams, IModelData, StartModelParams, SupportModel } from '@/types/model';
import { AxiosRequestConfig } from 'axios';
import { DELETE, GET, POST, PUT } from '.';
import { UpdatePromptParams, IPrompt, PromptParams } from '@/types/prompt';
import {
IFlow,
IFlowNode,
IFlowResponse,
IFlowUpdateParam,
IFlowRefreshParams,
IFlowExportParams,
IFlowImportParams,
IUploadFileRequestParams,
IUploadFileResponse,
} from '@/types/flow';
import { IAgent, IApp, IAppData } from '@/types/app';
/** App */
export const postScenes = () => {
@@ -273,23 +285,63 @@ export const postChatFeedBackForm = ({ data, config }: { data: ChatFeedBackSchem
/** AWEL Flow */
export const addFlow = (data: IFlowUpdateParam) => {
return POST<IFlowUpdateParam, IFlow>('/api/v1/serve/awel/flows', data);
return POST<IFlowUpdateParam, IFlow>('/api/v2/serve/awel/flows', data);
};
export const getFlows = (page?: number, page_size?: number) => {
return GET<any, IFlowResponse>('/api/v2/serve/awel/flows', {
page,
page_size,
});
};
export const getFlowById = (id: string) => {
return GET<null, IFlow>(`/api/v1/serve/awel/flows/${id}`);
return GET<null, IFlow>(`/api/v2/serve/awel/flows/${id}`);
};
export const updateFlowById = (id: string, data: IFlowUpdateParam) => {
return PUT<IFlowUpdateParam, IFlow>(`/api/v1/serve/awel/flows/${id}`, data);
return PUT<IFlowUpdateParam, IFlow>(`/api/v2/serve/awel/flows/${id}`, data);
};
export const deleteFlowById = (id: string) => {
return DELETE<null, null>(`/api/v1/serve/awel/flows/${id}`);
return DELETE<null, null>(`/api/v2/serve/awel/flows/${id}`);
};
export const getFlowNodes = () => {
return GET<null, Array<IFlowNode>>(`/api/v1/serve/awel/nodes`);
return GET<null, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`);
};
export const refreshFlowNodeById = (data: IFlowRefreshParams) => {
return POST<IFlowRefreshParams, IFlowNode>('/api/v2/serve/awel/nodes/refresh', data);
};
export const debugFlow = (data: any) => {
return POST<any, IFlowNode>('/api/v2/serve/awel/flow/debug', data);
};
export const exportFlow = (data: IFlowExportParams) => {
return GET<IFlowExportParams, any>(`/api/v2/serve/awel/flow/export/${data.uid}`, data);
};
export const importFlow = (data: IFlowImportParams) => {
return POST<IFlowImportParams, any>('/api/v2/serve/awel/flow/import', data);
};
export const uploadFile = (data: IUploadFileRequestParams) => {
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/dbgpt', data);
};
export const downloadFile = (fileId: string) => {
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
};
// TODOwait for interface update
export const getFlowTemplateList = () => {
return GET<null, Array<any>>('/api/v2/serve/awel/flow/templates');
};
export const getFlowTemplateById = (id: string) => {
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
};
/** app */

View File

@@ -61,6 +61,7 @@ function GPTCard({
return icon;
}, [icon]);
// TODO: 算子资源标签
const tagNode = useMemo(() => {
if (!tags || !tags.length) return null;
return (

View File

@@ -122,7 +122,7 @@ const AddNodes: React.FC = () => {
className="flex items-center justify-center rounded-full left-4 top-4"
style={{ zIndex: 1050 }}
icon={<PlusOutlined />}
></Button>
/>
</Popover>
);
};

View 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}
</>
);
};

View 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}
</>
);
};

View File

@@ -0,0 +1,3 @@
export * from './save-flow-modal';
export * from './export-flow-modal';
export * from './import-flow-modal';

View 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}
</>
);
};

View File

@@ -4,21 +4,20 @@ import NodeParamHandler from './node-param-handler';
import classNames from 'classnames';
import { useState } from 'react';
import NodeHandler from './node-handler';
import { Popover, Tooltip } from 'antd';
import { Form, Popover, Tooltip } from 'antd';
import { CopyOutlined, DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { useReactFlow } from 'reactflow';
import IconWrapper from '../common/icon-wrapper';
import { getUniqueNodeId } from '@/utils/flow';
import { getUniqueNodeId, removeIndexFromNodeId } from '@/utils/flow';
import { cloneDeep } from 'lodash';
import { apiInterceptors, refreshFlowNodeById } from '@/client/api';
type CanvasNodeProps = {
data: IFlowNode;
};
const ICON_PATH_PREFIX = '/icons/node/';
function TypeLabel({ label }: { label: string }) {
return <div className="w-full h-8 bg-stone-100 dark:bg-zinc-700 px-2 flex items-center justify-center">{label}</div>;
return <div className="w-full h-8 align-middle font-semibold">{label}</div>;
}
const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
@@ -26,6 +25,7 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
const { inputs, outputs, parameters, flow_type: flowType } = node;
const [isHovered, setIsHovered] = useState(false);
const reactFlow = useReactFlow();
const [form] = Form.useForm();
function onHover() {
setIsHovered(true);
@@ -71,25 +71,68 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
reactFlow.setEdges((edges) => edges.filter((edge) => edge.source !== node.id && edge.target !== node.id));
}
function renderOutput(data: IFlowNode) {
if (flowType === 'operator' && outputs?.length > 0) {
return (
<>
<TypeLabel label="Outputs" />
{(outputs || []).map((output, index) => (
<NodeHandler key={`${data.id}_input_${index}`} node={data} data={output} type="source" label="outputs" index={index} />
))}
</>
);
} else if (flowType === 'resource') {
// resource nodes show output default
return (
<>
<TypeLabel label="Outputs" />
<NodeHandler key={`${data.id}_input_0`} node={data} data={data} type="source" label="outputs" index={0} />
</>
function updateCurrentNodeValue(changedKey: string, changedVal: any) {
parameters.forEach((item) => {
if (item.name === changedKey) {
item.value = changedVal;
}
});
}
async function updateDependsNodeValue(changedKey: string, changedVal: any) {
const dependParamNodes = parameters.filter(({ ui }) => ui?.refresh_depends?.includes(changedKey));
if (dependParamNodes?.length === 0) return;
dependParamNodes.forEach(async (item) => {
const params = {
id: removeIndexFromNodeId(data?.id),
type_name: data.type_name,
type_cls: data.type_cls,
flow_type: 'operator' as const,
refresh: [
{
name: item.name,
depends: [
{
name: changedKey,
value: changedVal,
has_value: true,
},
],
},
],
};
const [_, res] = await apiInterceptors(refreshFlowNodeById(params));
// update value of the node
if (res) {
reactFlow.setNodes((nodes) =>
nodes.map((n) => {
return n.id === node.id
? {
...n,
data: {
...n.data,
parameters: res.parameters,
},
}
: n;
}),
);
}
});
}
function onParameterValuesChange(changedValues: any, allValues: any) {
const [changedKey, changedVal] = Object.entries(changedValues)[0];
updateCurrentNodeValue(changedKey, changedVal);
if (changedVal) {
updateDependsNodeValue(changedKey, changedVal);
}
}
return (
@@ -101,11 +144,21 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
<IconWrapper className="hover:text-blue-500">
<CopyOutlined className="h-full text-lg cursor-pointer" onClick={copyNode} />
</IconWrapper>
<IconWrapper className="mt-2 hover:text-red-500">
<DeleteOutlined className="h-full text-lg cursor-pointer" onClick={deleteNode} />
</IconWrapper>
<IconWrapper className="mt-2">
<Tooltip title={<><p className="font-bold">{node.label}</p><p>{node.description}</p></>} placement="right">
<Tooltip
title={
<>
<p className="font-bold">{node.label}</p>
<p>{node.description}</p>
</>
}
placement="right"
>
<InfoCircleOutlined className="h-full text-lg cursor-pointer" />
</Tooltip>
</IconWrapper>
@@ -113,37 +166,60 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
}
>
<div
className={classNames('w-72 h-auto rounded-xl shadow-md p-0 border bg-white dark:bg-zinc-800 cursor-grab', {
className={classNames(
'w-80 h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
{
'border-blue-500': node.selected || isHovered,
'border-stone-400 dark:border-white': !node.selected && !isHovered,
'border-dashed': flowType !== 'operator',
'border-red-600': node.invalid,
})}
},
)}
onMouseEnter={onHover}
onMouseLeave={onLeave}
>
{/* icon and label */}
<div className="flex flex-row items-center p-2">
<div className="flex flex-row items-center">
<Image src={'/icons/node/vis.png'} width={24} height={24} alt="" />
<p className="ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap">{node.label}</p>
</div>
{inputs && inputs.length > 0 && (
<>
{inputs?.length > 0 && (
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Inputs" />
{(inputs || []).map((input, index) => (
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={input} type="target" label="inputs" index={index} />
<div className="flex flex-col space-y-2">
{inputs?.map((item, index) => (
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={item} type="target" label="inputs" index={index} />
))}
</>
</div>
</div>
)}
{parameters && parameters.length > 0 && (
<>
{parameters?.length > 0 && (
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Parameters" />
{(parameters || []).map((parameter, index) => (
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} data={parameter} label="parameters" index={index} />
<Form form={form} layout="vertical" onValuesChange={onParameterValuesChange} className="flex flex-col space-y-3 text-neutral-500">
{parameters?.map((item, index) => (
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} paramData={item} label="parameters" index={index} />
))}
</>
</Form>
</div>
)}
{outputs?.length > 0 && (
<div className="bg-zinc-100 dark:bg-zinc-700 rounded p-2">
<TypeLabel label="Outputs" />
{flowType === 'operator' ? (
<div className="flex flex-col space-y-3">
{outputs.map((item, index) => (
<NodeHandler key={`${node.id}_output_${index}`} node={node} data={item} type="source" label="outputs" index={index} />
))}
</div>
) : (
flowType === 'resource' && <NodeHandler key={`${data.id}_output_0`} node={node} data={node} type="source" label="outputs" index={0} />
)}
</div>
)}
{renderOutput(node)}
</div>
</Popover>
);

View File

@@ -94,15 +94,15 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
})}
>
<Handle
className="w-2 h-2"
className={classNames('w-2 h-2', type === 'source' ? '-mr-4' : '-ml-4')}
type={type}
position={type === 'source' ? Position.Right : Position.Left}
id={`${node.id}|${label}|${index}`}
isValidConnection={(connection) => isValidConnection(connection)}
/>
<Typography
className={classNames('p-2', {
'pr-4': label === 'outputs',
className={classNames('bg-white dark:bg-[#232734] w-full px-2 py-1 rounded text-neutral-500', {
'text-right': label === 'outputs',
})}
>
<Popconfirm
@@ -117,9 +117,10 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
</div>
}
>
{['inputs', 'parameters'].includes(label) && <PlusOutlined className="mr-2 cursor-pointer" onClick={showRelatedNodes} />}
{['inputs', 'parameters'].includes(label) && <PlusOutlined className="cursor-pointer" onClick={showRelatedNodes} />}
</Popconfirm>
{data.type_name}:{label !== 'outputs' && <RequiredIcon optional={data.optional} />}
{label !== 'outputs' && <RequiredIcon optional={data.optional} />}
{data.type_name}
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />

View File

@@ -1,102 +1,150 @@
import { IFlowNode, IFlowNodeParameter } from '@/types/flow';
import { Checkbox, Input, InputNumber, Select, Tooltip } from 'antd';
import { Checkbox, Form, Input, InputNumber, Select } from 'antd';
import React from 'react';
import RequiredIcon from './required-icon';
import NodeHandler from './node-handler';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
renderSelect,
renderCheckbox,
renderRadio,
renderCascader,
renderDatePicker,
renderInput,
renderSlider,
renderTreeSelect,
renderTimePicker,
renderTextArea,
renderUpload,
renderCodeEditor,
renderPassword,
renderVariables,
} from './node-renderer';
interface NodeParamHandlerProps {
node: IFlowNode;
data: IFlowNodeParameter;
paramData: IFlowNodeParameter;
label: 'inputs' | 'outputs' | 'parameters';
index: number; // index of array
}
// render node parameters item
const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label, index }) => {
function handleChange(value: any) {
data.value = value;
}
const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, paramData, label, index }) => {
// render node parameters based on AWEL1.0
function renderNodeWithoutUiParam(data: IFlowNodeParameter) {
let defaultValue = data.value ?? data.default;
if (data.category === 'resource') {
return <NodeHandler node={node} data={data} type="target" label={label} index={index} />;
} else if (data.category === 'common') {
let defaultValue = data.value !== null && data.value !== undefined ? data.value : data.default;
switch (data.type_name) {
case 'int':
case 'float':
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
</p>
<InputNumber
className="w-full"
defaultValue={defaultValue}
onChange={(value: number | null) => {
handleChange(value);
}}
/>
</div>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
<InputNumber className="w-full nodrag" />
</Form.Item>
);
case 'str':
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
</p>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
{data.options?.length > 0 ? (
<Select
className="w-full nodrag"
defaultValue={defaultValue}
options={data.options.map((item: any) => ({ label: item.label, value: item.value }))}
onChange={handleChange}
/>
<Select className="w-full nodrag" options={data.options.map((item: any) => ({ label: item.label, value: item.value }))} />
) : (
<Input
className="w-full"
defaultValue={defaultValue}
onChange={(e) => {
handleChange(e.target.value);
}}
/>
<Input className="w-full" />
)}
</div>
</Form.Item>
);
case 'bool':
defaultValue = defaultValue === 'False' ? false : defaultValue;
defaultValue = defaultValue === 'True' ? true : defaultValue;
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
<Checkbox
className="ml-2"
defaultChecked={defaultValue}
onChange={(e) => {
handleChange(e.target.checked);
}}
/>
</p>
</div>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
<Checkbox className="ml-2" />
</Form.Item>
);
}
}
function renderComponentByType(type: string, data: IFlowNodeParameter) {
switch (type) {
case 'select':
return renderSelect(data);
case 'cascader':
return renderCascader(data);
case 'checkbox':
return renderCheckbox(data);
case 'radio':
return renderRadio(data);
case 'input':
return renderInput(data);
case 'text_area':
return renderTextArea(data);
case 'slider':
return renderSlider(data);
case 'date_picker':
return renderDatePicker(data);
case 'time_picker':
return renderTimePicker(data);
case 'tree_select':
return renderTreeSelect(data);
case 'password':
return renderPassword(data);
case 'upload':
return renderUpload({ data });
case 'variables':
return renderVariables(data);
case 'code_editor':
return renderCodeEditor(data);
default:
return null;
}
}
// render node parameters based on AWEL2.0
function renderNodeWithUiParam(data: IFlowNodeParameter) {
const { refresh_depends, ui_type } = data.ui;
let defaultValue = data.value ?? data.default;
return (
<Form.Item
className="mb-2"
initialValue={defaultValue}
name={data.name}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
{...(refresh_depends && { dependencies: refresh_depends })}
{...(data.description && { tooltip: { title: data.description, icon: <InfoCircleOutlined /> } })}
>
{renderComponentByType(ui_type, data)}
</Form.Item>
);
}
if (paramData.category === 'resource') {
return <NodeHandler node={node} data={paramData} type="target" label={label} index={index} />;
} else if (paramData.category === 'common') {
return paramData?.ui ? renderNodeWithUiParam(paramData) : renderNodeWithoutUiParam(paramData);
}
};
export default NodeParamHandler;

View File

@@ -0,0 +1,16 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Cascader } from 'antd';
export const renderCascader = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<Cascader
{...attr}
options={data.options}
placeholder="please select"
className="w-full nodrag"
/>
);
};

View File

@@ -0,0 +1,15 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Checkbox } from 'antd';
export const renderCheckbox = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
data.options?.length > 0 && (
<div className="bg-white p-2 rounded">
<Checkbox.Group {...attr} options={data.options} />
</div>
)
);
};

View File

@@ -0,0 +1,62 @@
import React, { useState, useMemo } from 'react';
import { Button, Form, Modal } from 'antd';
import Editor from '@monaco-editor/react';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { useTranslation } from 'react-i18next';
type Props = {
data: IFlowNodeParameter;
defaultValue?: any;
};
export const renderCodeEditor = (data: IFlowNodeParameter) => {
const { t } = useTranslation();
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = () => {
setIsModalOpen(true);
};
const onOk = () => {
setIsModalOpen(false);
};
const onCancel = () => {
setIsModalOpen(false);
};
const modalWidth = useMemo(() => {
if (data?.ui?.editor?.width) {
return data?.ui?.editor?.width + 100;
}
return '80%';
}, [data?.ui?.editor?.width]);
return (
<div className="p-2 text-sm">
<Button type="default" onClick={showModal}>
{t('Open_Code_Editor')}
</Button>
<Modal title={t('Code_Editor')} width={modalWidth} open={isModalOpen} onOk={onOk} onCancel={onCancel}>
<Form.Item name={data?.name}>
<Editor
{...attr}
width={data?.ui?.editor?.width || '100%'}
height={data?.ui?.editor?.height || 200}
defaultLanguage={data?.ui?.language}
theme="vs-dark"
options={{
minimap: {
enabled: false,
},
wordWrap: 'on',
}}
/>
</Form.Item>
</Modal>
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { DatePicker } from 'antd';
export const renderDatePicker = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <DatePicker {...attr} className="w-full" placeholder="please select a date" />;
};

View File

@@ -0,0 +1,14 @@
export * from './select';
export * from './cascader';
export * from './date-picker';
export * from './input';
export * from './checkbox';
export * from './radio';
export * from './textarea';
export * from './slider';
export * from './time-picker';
export * from './tree-select';
export * from './code-editor';
export * from './upload';
export * from './password';
export * from './variables';

View File

@@ -0,0 +1,22 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Input } from 'antd';
import * as Icons from '@ant-design/icons';
const getIconComponent = (iconString: string) => {
const match = iconString.match(/^icon:(\w+)$/);
if (match) {
const iconName = match[1] as keyof typeof Icons;
const IconComponent = Icons[iconName];
// @ts-ignore
return IconComponent ? <IconComponent /> : null;
}
return null;
};
export const renderInput = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
attr.prefix = getIconComponent(data.ui?.attr?.prefix || '');
return <Input {...attr} className="w-full" placeholder="please input" allowClear />;
};

View File

@@ -0,0 +1,11 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
const { Password } = Input;
export const renderPassword = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Password {...attr} placeholder="input password" />;
};

View File

@@ -0,0 +1,13 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Radio } from 'antd';
export const renderRadio = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="bg-white p-2 rounded">
<Radio.Group {...attr} options={data.options} />
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Select } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
export const renderSelect = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data?.ui?.attr || {});
return <Select {...attr} className="w-full nodrag" placeholder="please select" options={data.options} />;
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Slider } from 'antd';
export const renderSlider = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Slider className="mt-8 nodrag" {...attr} tooltip={{ open: true }} />;
};

View File

@@ -0,0 +1,16 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
import classNames from 'classnames';
const { TextArea } = Input;
export const renderTextArea = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className={classNames({ 'mb-3': attr.showCount === true })}>
<TextArea className="nowheel" {...attr} />
</div>
);
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { TimePicker } from 'antd';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
export const renderTimePicker = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <TimePicker {...attr} className="w-full" placeholder="please select a moment" />;
};

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { TreeSelect } from 'antd';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
export const renderTreeSelect = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<TreeSelect
{...attr}
className="w-full nodrag"
treeDefaultExpandAll
treeData={data.options}
/>
);
};

View File

@@ -0,0 +1,81 @@
import React, { useState, useRef } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload, message } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
import { IFlowNodeParameter } from '@/types/flow';
import { useTranslation } from 'react-i18next';
type Props = {
data: IFlowNodeParameter;
onChange?: (value: any) => void;
};
export const renderUpload = (params: Props) => {
const { t } = useTranslation();
const urlList = useRef<string[]>([]);
const { data, onChange } = params;
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const [uploading, setUploading] = useState(false);
const [uploadType, setUploadType] = useState('');
const getUploadSuccessUrl = (url: string) => {
if (urlList.current.length === data.ui.attr.max_count) {
urlList.current.pop();
}
urlList.current.push(url);
onChange?.(urlList.current.toString());
};
const handleFileRemove = (file: any) => {
const index = urlList.current.indexOf(file.response.data[0].uri);
if (index !== -1) {
urlList.current.splice(index, 1);
}
onChange?.(urlList.current.toString());
};
const props: UploadProps = {
name: 'files',
action: process.env.API_BASE_URL + data.ui.action,
headers: {
authorization: 'authorization-text',
},
onChange(info) {
setUploading(true);
if (info.file.status !== 'uploading') {
}
if (info.file.status === 'done') {
setUploading(false);
message.success(`${info.file.response.data[0].file_name} ${t('Upload_Data_Successfully')}`);
getUploadSuccessUrl(info.file.response.data[0].uri);
} else if (info.file.status === 'error') {
setUploading(false);
message.error(`${info.file.response.data[0].file_name} ${t('Upload_Data_Failed')}`);
}
},
};
if (data.ui?.file_types && Array.isArray(data.ui?.file_types)) {
setUploadType(data.ui?.file_types.toString());
}
return (
<div className="p-2 text-sm text-center">
{data.is_list ? (
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={true} accept={uploadType}>
<Button loading={uploading} icon={<UploadOutlined />}>
{t('Upload_Data')}
</Button>
</Upload>
) : (
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={false} accept={uploadType}>
<Button loading={uploading} icon={<UploadOutlined />}>
{t('Upload_Data')}
</Button>
</Upload>
)}
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Input } from 'antd';
export const renderVariables = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Input {...attr} className="w-full" placeholder="please input" allowClear />;
};

View File

@@ -39,6 +39,7 @@
"framer-motion": "^10.16.4",
"google-auth-library": "^9.2.0",
"google-one-tap": "^1.0.6",
"dayjs": "^1.11.12",
"i18next": "^23.4.5",
"iron-session": "^6.3.1",
"lodash": "^4.17.21",

View File

@@ -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 AddNodes from '@/components/flow/add-nodes';
import ButtonEdge from '@/components/flow/button-edge';
import CanvasNode from '@/components/flow/canvas-node';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
import { FrownOutlined, SaveOutlined } from '@ant-design/icons';
import { Button, Checkbox, Divider, Form, Input, Modal, Space, message, notification } from 'antd';
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
import { ExportOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
import { Divider, Space, Tooltip, message, notification } from 'antd';
import { useSearchParams } from 'next/navigation';
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
import 'reactflow/dist/style.css';
const { TextArea } = Input;
import { SaveFlowModal, ExportFlowModal, ImportFlowModal } from '@/components/flow/canvas-modal';
interface Props {
// Define your component props here
@@ -23,8 +22,7 @@ const edgeTypes = { buttonedge: ButtonEdge };
const Canvas: React.FC<Props> = () => {
const { t } = useTranslation();
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<IFlowUpdateParam>();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const reactFlow = useReactFlow();
@@ -33,9 +31,10 @@ const Canvas: React.FC<Props> = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
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() {
setLoading(true);
@@ -139,18 +138,7 @@ const Canvas: React.FC<Props> = () => {
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 clickSave() {
function onSave() {
const flowData = reactFlow.toObject() as IFlowData;
const [check, node, message] = checkFlowDataRequied(flowData);
if (!check && message) {
@@ -172,39 +160,36 @@ const Canvas: React.FC<Props> = () => {
);
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
}
setIsModalVisible(true);
setIsSaveFlowModalOpen(true);
}
async function handleSaveFlow() {
const { name, label, description = '', editable = false, state = 'deployed' } = 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);
function onExport() {
setIsExportFlowModalOpen(true);
}
function onImport() {
setIsImportFlowModalOpen(true);
}
return (
<>
<MuiLoading visible={loading} />
<div className="my-2 mx-4 flex flex-row justify-end items-center">
<div className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 flext justify-center items-center hover:text-blue-500 dark:hover:text-zinc-100">
<SaveOutlined className="block text-xl" onClick={clickSave} />
</div>
</div>
<Space className="my-2 mx-4 flex flex-row justify-end">
{[
{ title: 'import', icon: <ImportOutlined className="block text-xl" onClick={onImport} /> },
{ title: 'export', icon: <ExportOutlined className="block text-xl" onClick={onExport} /> },
{ title: 'save', icon: <SaveOutlined className="block text-xl" onClick={onSave} /> },
].map(({ title, icon }) => (
<Tooltip
key={title}
title={title}
className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 hover:text-blue-500 dark:hover:text-zinc-100"
>
{icon}
</Tooltip>
))}
</Space>
<Divider className="mt-0 mb-0" />
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
<ReactFlow
@@ -227,85 +212,27 @@ const Canvas: React.FC<Props> = () => {
<AddNodes />
</ReactFlow>
</div>
<Modal
title={t('flow_modal_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name="flow_form"
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={handleSaveFlow}
autoComplete="off"
>
<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);
}}
<SaveFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isSaveFlowModalOpen={isSaveFlowModalOpen}
setIsSaveFlowModalOpen={setIsSaveFlowModalOpen}
/>
<ExportFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isExportFlowModalOpen={isExportFlowModalOpen}
setIsExportFlowModalOpen={setIsExportFlowModalOpen}
/>
<ImportFlowModal
setNodes={setNodes}
setEdges={setEdges}
isImportModalOpen={isImportModalOpen}
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
/>
</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}
</>
);
};

View File

@@ -4,7 +4,7 @@ import MuiLoading from '@/components/common/loading';
import FlowCard from '@/components/flow/flow-card';
import { IFlow, IFlowUpdateParam } from '@/types/flow';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Checkbox, Form, Input, Modal, message } from 'antd';
import { Button, Checkbox, Form, Input, Modal, message, Pagination, PaginationProps } from 'antd';
import Link from 'next/link';
import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,22 +16,25 @@ function Flow() {
const [loading, setLoading] = useState(false);
const [flowList, setFlowList] = useState<Array<IFlow>>([]);
const [deploy, setDeploy] = useState(false);
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [total, setTotal] = useState<number>(0);
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
const copyFlowTemp = useRef<IFlow>();
async function getFlowList() {
setLoading(true);
const [_, data] = await apiInterceptors(getFlows());
const [_, data] = await apiInterceptors(getFlows(page, pageSize));
setTotal(data?.total_count ?? 0);
setLoading(false);
setFlowList(data?.items ?? []);
}
useEffect(() => {
getFlowList();
}, []);
}, [page, pageSize]);
function updateFlowList(uid: string) {
setFlowList((flows) => flows.filter((flow) => flow.uid !== uid));
@@ -62,10 +65,16 @@ function Flow() {
}
};
const onPageChange: PaginationProps['onChange'] = (page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
};
return (
<div className="relative p-4 md:p-6 min-h-full overflow-y-auto">
<div className="flex flex-col justify-between relative p-4 md:p-6 min-h-full overflow-y-auto">
{contextHolder}
<MuiLoading visible={loading} />
<div className="mb-4">
<Link href="/flow/canvas">
<Button type="primary" className="flex items-center" icon={<PlusOutlined />}>
@@ -73,12 +82,29 @@ function Flow() {
</Button>
</Link>
</div>
<div className="flex flex-col justify-between flex-1">
<div className="flex flex-wrap gap-2 md:gap-4 justify-start items-stretch">
{flowList.map((flow) => (
<FlowCard key={flow.uid} flow={flow} deleteCallback={updateFlowList} onCopy={handleCopy} />
))}
{flowList.length === 0 && <MyEmpty description="No flow found" />}
</div>
<div className="flex justify-end mt-6">
<Pagination
showQuickJumper
showSizeChanger
defaultPageSize={10}
defaultCurrent={1}
current={page}
total={total}
showTotal={(total) => `Total ${total} items`}
onChange={onPageChange}
/>
</div>
</div>
<Modal
open={showModal}
title="Copy AWEL Flow"

View File

@@ -1,3 +1,4 @@
import { File } from 'buffer';
import { Node } from 'reactflow';
export type FlowState = 'deployed' | 'developing' | 'initializing' | 'testing' | 'disabled' | 'running' | 'load_failed';
@@ -13,6 +14,21 @@ export type IFlowUpdateParam = {
state?: FlowState;
};
export type IFlowRefreshParams = {
id: string;
type_name: string;
type_cls: string;
flow_type: 'resource' | 'operator';
refresh: {
name: string;
depends?: Array<{
name: string;
value: any;
has_value: boolean;
}>;
}[];
};
export type IFlow = {
dag_id: string;
gmt_created: string;
@@ -52,6 +68,25 @@ export type IFlowNodeParameter = {
options?: any;
value: any;
is_list?: boolean;
ui: IFlowNodeParameterUI;
};
export type IFlowNodeParameterUI = {
ui_type: string;
language: string;
file_types: string;
action: string;
attr: {
disabled: boolean;
[key: string]: any;
};
editor?: {
width: number;
height: number;
};
show_input?: boolean;
refresh?: boolean;
refresh_depends?: string[];
};
export type IFlowNodeInput = {
@@ -139,7 +174,29 @@ export type IFlowData = {
viewport: IFlowDataViewport;
};
export interface UpdateFLowAdminsParams {
export type IFlowExportParams = {
uid: string;
admins: string[];
}
export_type?: 'json' | 'dbgpts';
format?: 'json' | 'file';
file_name?: string;
user_name?: string;
sys_code?: string;
};
export type IFlowImportParams = {
file: File;
save_flow?: boolean;
};
export type IUploadFileRequestParams = {
files: Array<File>;
user_name?: string;
sys_code?: string;
};
export type IUploadFileResponse = {
file_name: string;
file_id: string;
bucket: string;
uri?: string;
};

View File

@@ -11,6 +11,12 @@ export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
return `${nodeData.id}_${count}`;
};
// function getUniqueNodeId will add '_${count}' to id, so we need to remove it when we want to get the original id
export const removeIndexFromNodeId = (id: string) => {
const indexPattern = /_\d+$/;
return id.replace(indexPattern, '');
};
// 驼峰转下划线,接口协议字段命名规范
export const mapHumpToUnderline = (flowData: IFlowData) => {
/**
@@ -98,3 +104,31 @@ export const checkFlowDataRequied = (flowData: IFlowData) => {
}
return result;
};
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
function toCamelCase(str: string): string {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
function isObject(value: any): boolean {
return value && typeof value === 'object' && !Array.isArray(value);
}
function convert(obj: any): any {
if (Array.isArray(obj)) {
return obj.map((item) => convert(item));
} else if (isObject(obj)) {
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = toCamelCase(key);
newObj[newKey] = convert(obj[key]);
}
}
return newObj;
}
return obj;
}
return convert(obj);
};

6727
web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff