feat: Update AddFlowVariableModal component to include parameter management (#1963)

This commit is contained in:
Dreammy23 2024-09-04 18:51:23 +08:00 committed by GitHub
commit af09d14d4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 56 deletions

View File

@ -6,6 +6,7 @@ import {
IFlowRefreshParams,
IFlowResponse,
IFlowUpdateParam,
IFlowVariablesParams,
IUploadFileRequestParams,
IUploadFileResponse,
} from '@/types/flow';
@ -63,7 +64,6 @@ 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');
};
@ -71,6 +71,15 @@ export const getFlowTemplateList = () => {
export const getFlowTemplateById = (id: string) => {
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
};
export const getKeys = () => {
return GET<null, Array<any>>('/api/v2/serve/awel/variables/keys');
};
export const getVariablesByKey = ({ key, scope }: { key: string; scope: string }) => {
return GET<IFlowVariablesParams, any>('/api/v2/serve/awel/variables', { key, scope });
};
export const metadataBatch = (data: IUploadFileRequestParams) => {
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/metadata/batch', data);
};
};

View File

@ -1,64 +1,128 @@
// import { IFlowNode } from '@/types/flow';
import { apiInterceptors, getKeys, getVariablesByKey } from '@/client/api';
import { IVariableInfo } from '@/types/flow';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Select, Space } from 'antd';
import React, { useState } from 'react';
import { Button, Cascader, Form, Input, Modal, Select, Space } from 'antd';
import { uniqBy } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
// ype GroupType = { category: string; categoryLabel: string; nodes: IFlowNode[] };
type ValueType = 'str' | 'int' | 'float' | 'bool' | 'ref';
const { Option } = Select;
type ValueType = 'str' | 'int' | 'float' | 'bool' | 'ref';
interface Option {
value?: string | number | null;
label: React.ReactNode;
children?: Option[];
isLeaf?: boolean;
}
interface VariableDict {
key: string;
name?: string;
scope?: string;
scope_key?: string;
sys_code?: string;
user_name?: string;
}
const DAG_PARAM_KEY = 'dbgpt.core.flow.params';
const DAG_PARAM_SCOPE = 'flow_priv';
export const AddFlowVariableModal: React.FC = () => {
const { t } = useTranslation();
// const [operators, setOperators] = useState<Array<IFlowNode>>([]);
// const [resources, setResources] = useState<Array<IFlowNode>>([]);
// const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
// const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [form] = Form.useForm(); // const [form] = Form.useForm<IFlowUpdateParam>();
function escapeVariable(value: string, enableEscape: boolean): string {
if (!enableEscape) {
return value;
}
return value.replace(/@/g, '\\@').replace(/#/g, '\\#').replace(/%/g, '\\%').replace(/:/g, '\\:');
}
const showModal = () => {
setIsModalOpen(true);
function buildVariableString(variableDict) {
const scopeSig = '@';
const sysCodeSig = '#';
const userSig = '%';
const kvSig = ':';
const enableEscape = true;
const specialChars = new Set([scopeSig, sysCodeSig, userSig, kvSig]);
// Replace undefined or null with ""
const newVariableDict: VariableDict = {
key: variableDict.key || '',
name: variableDict.name || '',
scope: variableDict.scope || '',
scope_key: variableDict.scope_key || '',
sys_code: variableDict.sys_code || '',
user_name: variableDict.user_name || '',
};
// TODO: get keys
// useEffect(() => {
// getNodes();
// }, []);
// Check for special characters in values
for (const [key, value] of Object.entries(newVariableDict)) {
if (value && [...specialChars].some(char => value.includes(char))) {
if (enableEscape) {
newVariableDict[key as keyof VariableDict] = escapeVariable(value, enableEscape);
} else {
throw new Error(
`${key} contains special characters, error value: ${value}, special characters: ${[...specialChars].join(', ')}`,
);
}
}
}
// async function getNodes() {
// const [_, data] = await apiInterceptors(getFlowNodes());
// if (data && data.length > 0) {
// localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
// const operatorNodes = data.filter(node => node.flow_type === 'operator');
// const resourceNodes = data.filter(node => node.flow_type === 'resource');
// setOperators(operatorNodes);
// setResources(resourceNodes);
// setOperatorsGroup(groupNodes(operatorNodes));
// setResourcesGroup(groupNodes(resourceNodes));
// }
// }
const { key, name, scope, scope_key, sys_code, user_name } = newVariableDict;
// function groupNodes(data: IFlowNode[]) {
// const groups: GroupType[] = [];
// const categoryMap: Record<string, { category: string; categoryLabel: string; nodes: IFlowNode[] }> = {};
// data.forEach(item => {
// const { category, category_label } = item;
// if (!categoryMap[category]) {
// categoryMap[category] = { category, categoryLabel: category_label, nodes: [] };
// groups.push(categoryMap[category]);
// }
// categoryMap[category].nodes.push(item);
// });
// return groups;
// }
let variableStr = `${key}`;
if (name) {
variableStr += `${kvSig}${name}`;
}
if (scope) {
variableStr += `${scopeSig}${scope}`;
if (scope_key) {
variableStr += `${kvSig}${scope_key}`;
}
}
if (sys_code) {
variableStr += `${sysCodeSig}${sys_code}`;
}
if (user_name) {
variableStr += `${userSig}${user_name}`;
}
return `\${${variableStr}}`;
}
export const AddFlowVariableModal: React.FC = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [form] = Form.useForm();
const [controlTypes, setControlTypes] = useState<ValueType[]>(['str']);
const [refVariableOptions, setRefVariableOptions] = useState<Option[]>([]);
useEffect(() => {
getKeysData();
}, []);
const getKeysData = async () => {
const [err, res] = await apiInterceptors(getKeys());
if (err) return;
const keyOptions = res?.map(({ key, label, scope }: IVariableInfo) => ({
value: key,
label,
scope,
isLeaf: false,
}));
setRefVariableOptions(keyOptions);
};
const onFinish = (values: any) => {
console.log('Received values of form:', values);
const variables = JSON.stringify(values.parameters);
localStorage.setItem('variables', variables);
setIsModalOpen(false);
};
function onNameChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
@ -95,13 +159,47 @@ export const AddFlowVariableModal: React.FC = () => {
}
function onValueTypeChange(type: ValueType, index: number) {
if (type === 'ref') {
const newControlTypes = [...controlTypes];
newControlTypes[index] = type;
setControlTypes(newControlTypes);
}
function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1];
const { value, scope } = targetOption as Option & { scope: string };
setTimeout(async () => {
const [err, res] = await apiInterceptors(getVariablesByKey({ key: value as string, scope }));
if (err) return;
if (res?.total_count === 0) {
targetOption.isLeaf = true;
return;
}
const uniqueItems = uniqBy(res?.items, 'name');
targetOption.children = uniqueItems?.map(item => ({
value: item?.name,
label: item.label,
data: item,
}));
setRefVariableOptions([...refVariableOptions]);
}, 1000);
}
function onRefTypeValueChange(value: string[], selectedOptions: Option[], index: number) {
// 选择两个select后获取到的value才能设置引用变量的值
if (value?.length === 2) {
const [selectRefKey, selectedRefVariable] = selectedOptions;
const selectedVariableData = selectRefKey?.children?.find(({ value }) => value === selectedRefVariable?.value);
const variableStr = buildVariableString(selectedVariableData?.data);
const parameters = form.getFieldValue('parameters');
const param = parameters?.[index];
if (param) {
const { name = '' } = param;
param.value = `${DAG_PARAM_KEY}:${name}@scope:${DAG_PARAM_SCOPE}`;
param.value = variableStr;
param.category = selectedVariableData?.data?.category;
param.value_type = selectedVariableData?.data?.value_type;
form.setFieldsValue({
parameters: [...parameters],
@ -117,7 +215,7 @@ export const AddFlowVariableModal: React.FC = () => {
className='flex items-center justify-center rounded-full left-4 top-4'
style={{ zIndex: 1050 }}
icon={<PlusOutlined />}
onClick={showModal}
onClick={() => setIsModalOpen(true)}
/>
<Modal
@ -125,6 +223,7 @@ export const AddFlowVariableModal: React.FC = () => {
open={isModalOpen}
footer={null}
width={1000}
onCancel={() => setIsModalOpen(false)}
styles={{
body: {
maxHeight: '70vh',
@ -134,7 +233,6 @@ export const AddFlowVariableModal: React.FC = () => {
borderRadius: 4,
},
}}
onClose={() => setIsModalOpen(false)}
>
<Form
name='dynamic_form_nest_item'
@ -199,13 +297,29 @@ export const AddFlowVariableModal: React.FC = () => {
style={{ width: 320 }}
rules={[{ required: true, message: 'Missing parameter value' }]}
>
<Input placeholder='Parameter Value' />
{controlTypes[index] === 'ref' ? (
<Cascader
placeholder='Select Value'
options={refVariableOptions}
loadData={loadData}
onChange={(value, selectedOptions) => onRefTypeValueChange(value, selectedOptions, index)}
// displayRender={displayRender}
// dropdownRender={dropdownRender}
changeOnSelect
/>
) : (
<Input placeholder='Parameter Value' />
)}
</Form.Item>
<Form.Item {...restField} name={[name, 'description']} label='描述' style={{ width: 170 }}>
<Input placeholder='Parameter Description' />
</Form.Item>
<Form.Item name={[name, 'key']} hidden initialValue='dbgpt.core.flow.params' />
<Form.Item name={[name, 'scope']} hidden initialValue='flow_priv' />
<Form.Item name={[name, 'category']} hidden initialValue='common' />
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}

View File

@ -92,7 +92,7 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
</Upload>
</Form.Item>
<Form.Item name='save_flow' label={t('Save_After_Import')}>
<Form.Item name='save_flow' label={t('Save_After_Import')} hidden>
<Radio.Group>
<Radio value={true}>{t('Yes')}</Radio>
<Radio value={false}>{t('No')}</Radio>

View File

@ -47,6 +47,8 @@ export const SaveFlowModal: React.FC<Props> = ({
async function onSaveFlow() {
const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue();
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
const variableValues = localStorage.getItem('variables');
const variables = JSON.parse(variableValues || '[]');
if (id) {
const [, , res] = await apiInterceptors(
@ -58,6 +60,7 @@ export const SaveFlowModal: React.FC<Props> = ({
uid: id.toString(),
flow_data: reactFlowObject,
state,
variables,
}),
);
@ -75,6 +78,7 @@ export const SaveFlowModal: React.FC<Props> = ({
editable,
flow_data: reactFlowObject,
state,
variables,
}),
);

View File

@ -12,6 +12,7 @@ export type IFlowUpdateParam = {
uid?: string;
flow_data?: IFlowData;
state?: FlowState;
variables?: IVariableInfo[];
};
export type IFlowRefreshParams = {
@ -200,3 +201,23 @@ export type IUploadFileResponse = {
bucket: string;
uri?: string;
};
export type IFlowVariablesParams = {
key: string;
scope: string;
scope_key?: string;
user_name?: string;
sys_code?: string;
page?: number;
page_size?: number;
};
export type IVariableInfo = {
key: string;
label: string;
description: string;
value_type: string;
category: string;
scope: string;
scope_key: string | null;
};