mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-11 22:09:44 +00:00
Merge branch 'feat/sprint-web-flow' into dev-0.6
This commit is contained in:
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { Domain } from '@mui/icons-material';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import en from '@/locales/en';
|
||||
|
@@ -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}`);
|
||||
};
|
||||
|
||||
// TODO:wait 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 */
|
||||
|
@@ -61,6 +61,7 @@ function GPTCard({
|
||||
return icon;
|
||||
}, [icon]);
|
||||
|
||||
// TODO: 算子资源标签
|
||||
const tagNode = useMemo(() => {
|
||||
if (!tags || !tags.length) return null;
|
||||
return (
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
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}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
|
@@ -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" />
|
||||
|
@@ -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;
|
||||
|
16
web/components/flow/node-renderer/cascader.tsx
Normal file
16
web/components/flow/node-renderer/cascader.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
};
|
15
web/components/flow/node-renderer/checkbox.tsx
Normal file
15
web/components/flow/node-renderer/checkbox.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
};
|
62
web/components/flow/node-renderer/code-editor.tsx
Normal file
62
web/components/flow/node-renderer/code-editor.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/date-picker.tsx
Normal file
9
web/components/flow/node-renderer/date-picker.tsx
Normal 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" />;
|
||||
};
|
14
web/components/flow/node-renderer/index.ts
Normal file
14
web/components/flow/node-renderer/index.ts
Normal 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';
|
22
web/components/flow/node-renderer/input.tsx
Normal file
22
web/components/flow/node-renderer/input.tsx
Normal 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 />;
|
||||
};
|
11
web/components/flow/node-renderer/password.tsx
Normal file
11
web/components/flow/node-renderer/password.tsx
Normal 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" />;
|
||||
};
|
13
web/components/flow/node-renderer/radio.tsx
Normal file
13
web/components/flow/node-renderer/radio.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/select.tsx
Normal file
9
web/components/flow/node-renderer/select.tsx
Normal 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} />;
|
||||
};
|
10
web/components/flow/node-renderer/slider.tsx
Normal file
10
web/components/flow/node-renderer/slider.tsx
Normal 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 }} />;
|
||||
};
|
16
web/components/flow/node-renderer/textarea.tsx
Normal file
16
web/components/flow/node-renderer/textarea.tsx
Normal 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>
|
||||
);
|
||||
};
|
10
web/components/flow/node-renderer/time-picker.tsx
Normal file
10
web/components/flow/node-renderer/time-picker.tsx
Normal 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" />;
|
||||
};
|
17
web/components/flow/node-renderer/tree-select.tsx
Normal file
17
web/components/flow/node-renderer/tree-select.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
81
web/components/flow/node-renderer/upload.tsx
Normal file
81
web/components/flow/node-renderer/upload.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/variables.tsx
Normal file
9
web/components/flow/node-renderer/variables.tsx
Normal 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 />;
|
||||
};
|
@@ -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",
|
||||
|
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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
6727
web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user