Browse Source

feat: 流水线必填项校验

pull/99/head
cp3hnu 1 year ago
parent
commit
5ee8a6bc17
10 changed files with 195 additions and 104 deletions
  1. BIN
      react-ui/src/assets/img/experiment-running.png
  2. BIN
      react-ui/src/assets/img/pipeline-warning.png
  3. +46
    -22
      react-ui/src/components/ParameterInput/index.tsx
  4. +1
    -1
      react-ui/src/pages/Experiment/Info/index.jsx
  5. +27
    -10
      react-ui/src/pages/ModelDeployment/Create/index.tsx
  6. +39
    -30
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  7. +2
    -1
      react-ui/src/pages/Pipeline/editPipeline/index.less
  8. +56
    -33
      react-ui/src/pages/Pipeline/editPipeline/props.tsx
  9. +23
    -0
      react-ui/src/utils/index.ts
  10. +1
    -7
      react-ui/src/utils/promise.ts

BIN
react-ui/src/assets/img/experiment-running.png View File

Before After
Width: 66  |  Height: 66  |  Size: 2.1 kB

BIN
react-ui/src/assets/img/pipeline-warning.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.7 kB

+ 46
- 22
react-ui/src/components/ParameterInput/index.tsx View File

@@ -1,18 +1,28 @@
import { CloseOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import './index.less';

type ParameterInputData = {
value?: any;
showValue?: any;
fromSelect?: boolean;
} & Record<string, any>;
// 对象
export type ParameterInputObject = {
value?: any; // 值
showValue?: any; // 显示值
fromSelect?: boolean; // 是否来自选择
activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab
expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys
checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys
[key: string]: any;
};

interface ParameterInputProps {
value?: ParameterInputData;
onChange?: (value: ParameterInputData) => void;
// 值类型
export type ParameterInputValue = ParameterInputObject | string;

export interface ParameterInputProps {
value?: ParameterInputValue;
onChange?: (value?: ParameterInputValue) => void;
onClick?: () => void;
onRemove?: () => void;
canInput?: boolean;
textArea?: boolean;
placeholder?: string;
@@ -27,6 +37,7 @@ function ParameterInput({
value,
onChange,
onClick,
onRemove,
canInput = true,
textArea = false,
allowClear,
@@ -42,8 +53,23 @@ function ParameterInput({
valueObj.showValue = valueObj.value;
}
const isSelect = valueObj?.fromSelect;
const InputComponent = textArea ? Input.TextArea : Input;
const placeholder = valueObj?.placeholder || rest?.placeholder;
const InputComponent = textArea ? Input.TextArea : Input;

// 删除
const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
e.stopPropagation();
onChange?.({
...valueObj,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
onRemove?.();
};

return (
<>
@@ -62,18 +88,7 @@ function ParameterInput({
<span className="parameter-input__content__value">{valueObj?.showValue}</span>
<CloseOutlined
className="parameter-input__content__close-icon"
onClick={(e) => {
e.stopPropagation();
onChange?.({
...valueObj,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: undefined,
checkedKeys: undefined,
});
}}
onClick={handleRemove}
/>
</div>
) : (
@@ -93,9 +108,9 @@ function ParameterInput({
onChange={(e) =>
onChange?.({
...valueObj,
fromSelect: false,
value: e.target.value,
showValue: e.target.value,
fromSelect: false,
})
}
/>
@@ -105,3 +120,12 @@ function ParameterInput({
}

export default ParameterInput;

// 必填校验
export const requiredValidator = (rule: RuleObject, value: any) => {
const trueValue = typeof value === 'object' ? value?.value : value;
if (!trueValue) {
return Promise.reject(rule.message || '必填项');
}
return Promise.resolve();
};

+ 1
- 1
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -19,7 +19,7 @@ function ExperimentText() {
const [message, setMessage, messageRef] = useStateRef({});
const propsRef = useRef();
const navgite = useNavigate();
const locationParams = useParams(); //新版本获取路由参数接口
const locationParams = useParams(); // 新版本获取路由参数接口
const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);
const graphRef = useRef();



+ 27
- 10
react-ui/src/pages/ModelDeployment/Create/index.tsx View File

@@ -5,7 +5,7 @@
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import ParameterInput from '@/components/ParameterInput';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
@@ -87,7 +87,7 @@ function ModelDeploymentCreate() {
};

// 选择模型、镜像
const selectResource = (name: string, type: ResourceSelectorType) => {
const selectResource = (formItemName: string, type: ResourceSelectorType) => {
let resource: ResourceSelectorResponse | undefined;
switch (type) {
case ResourceSelectorType.Model:
@@ -105,13 +105,25 @@ function ModelDeploymentCreate() {
onOk: (res) => {
if (res) {
if (type === ResourceSelectorType.Mirror) {
form.setFieldValue(name, res.path);
form.setFieldValue(formItemName, res.path);
setSelectedMirror(res);
} else {
const showValue = `${res.name}:${res.version}`;
form.setFieldValue(name, {
...pick(res, ['id', 'version', 'path']),
const { activeTab, id, name, version, path } = res;
const jsonObj = {
id,
version,
path,
};
const value = JSON.stringify(jsonObj);
const showValue = `${name}:${version}`;
form.setFieldValue(formItemName, {
value,
showValue,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
...jsonObj,
});
setSelectedModel(res);
}
@@ -121,8 +133,9 @@ function ModelDeploymentCreate() {
} else {
setSelectedMirror(undefined);
}
form.setFieldValue(name, '');
form.setFieldValue(formItemName, '');
}
form.validateFields([formItemName]);
close();
},
});
@@ -258,10 +271,11 @@ function ModelDeploymentCreate() {
name="model"
rules={[
{
required: true,
validator: requiredValidator,
message: '请选择模型',
},
]}
required
>
<ParameterInput
placeholder="请选择模型"
@@ -269,6 +283,7 @@ function ModelDeploymentCreate() {
canInput={false}
size="large"
onClick={() => selectResource('model', ResourceSelectorType.Model)}
onChange={() => setSelectedModel(undefined)}
/>
</Form.Item>
</Col>
@@ -291,16 +306,18 @@ function ModelDeploymentCreate() {
name="image"
rules={[
{
required: true,
message: '请输入镜像',
validator: requiredValidator,
message: '请选择镜像',
},
]}
required
>
<ParameterInput
placeholder="请选择镜像"
canInput={false}
size="large"
onClick={() => selectResource('image', ResourceSelectorType.Mirror)}
onChange={() => setSelectedMirror(undefined)}
/>
</Form.Item>
</Col>


+ 39
- 30
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -93,15 +93,17 @@ const EditPipeline = () => {
return;
}

const [propsRes, propsError] = await to(propsRef.current.getFieldsValue());
if (propsError) {
message.error('节点必填项必须配置');
return;
}
propsRef.current.propClose();
setTimeout(() => {
const data = graph.save();
console.log(data);
const errorNode = data.nodes.find((item) => {
return item.formError === true;
});
if (errorNode) {
message.error(`【${errorNode.label}】节点必填项必须配置`);
return;
}
const params = {
...locationParams,
dag: JSON.stringify(data),
@@ -298,7 +300,7 @@ const EditPipeline = () => {
);
},
afterDraw(cfg, group) {
const image = group.addShape('image', {
group.addShape('image', {
attrs: {
x: -45,
y: -10,
@@ -325,7 +327,26 @@ const EditPipeline = () => {
draggable: true,
});
}
if (cfg.formError) {
group.addShape('image', {
attrs: {
x: 43,
y: -24,
width: 18,
height: 18,
img: require('@/assets/img/pipeline-warning.png'),
cursor: 'pointer',
},
draggable: false,
capture: false,
});
}
const bbox = group.getBBox();
if (cfg.formError) {
bbox.y += 6;
bbox.width -= 6;
bbox.height -= 6;
}
const anchorPoints = this.getAnchorPoints(cfg);
anchorPoints.forEach((anchorPos, i) => {
group.addShape('circle', {
@@ -345,18 +366,10 @@ const EditPipeline = () => {
draggable: true,
});
});
return image;
},

// response the state changes and show/hide the link-point circles
setState(name, value, item) {
// const anchorPoints = item
// .getContainer()
// .findAll((ele) => ele.get('name') === 'anchor-point');
// anchorPoints.forEach((point) => {
// if (value || point.get('links') > 0) point.show();
// else point.hide();
// });
const group = item.getContainer();
const shape = group.get('children')[0];
const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point');
@@ -617,30 +630,26 @@ const EditPipeline = () => {
const cloneDisplay = type === 'node' ? 'flex' : 'none';
return `
<div>
<div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" code="clone">
<svg class="pipeline-context-menu__item__icon">
<div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone">
<svg class="pipeline-context-menu__item__icon" id="clone-svg">
<use xlink:href="#icon-fuzhi1" />
</svg>
<span>复制</span>
复制
</div>
<div class="pipeline-context-menu__item" code="delete">
<svg class="pipeline-context-menu__item__icon">
<div class="pipeline-context-menu__item" id="delete">
<svg class="pipeline-context-menu__item__icon" id="delete-svg">
<use xlink:href="#icon-shanchu1" />
</svg>
<span>删除</span>
删除
</div>
</div>`;
},
handleMenuClick: (target, item) => {
switch (target.getAttribute('code')) {
case 'delete':
graph.removeItem(item);
break;
case 'clone':
cloneElement(item);
break;
default:
break;
const id = target.id;
if (id.startsWith('clone')) {
cloneElement(item);
} else if (id.startsWith('delete')) {
graph.removeItem(item);
}
},
// offsetX and offsetY include the padding of the parent container
@@ -697,7 +706,7 @@ const EditPipeline = () => {
</div>
<div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div>
</div>
<Props ref={propsRef} onParentChange={handleFormChange}></Props>
<Props ref={propsRef} onFormChange={handleFormChange}></Props>
<GlobalParamsDrawer
ref={paramsDrawerRef}
open={paramsDrawerOpen}


+ 2
- 1
react-ui/src/pages/Pipeline/editPipeline/index.less View File

@@ -48,7 +48,8 @@
cursor: pointer;

&:hover {
color: @primary-color;
color: #0d5ef8;
font-weight: 500;
background-color: .addAlpha(#8895a8, 0.11) [];
}



+ 56
- 33
react-ui/src/pages/Pipeline/editPipeline/props.tsx View File

@@ -1,5 +1,5 @@
import KFIcon from '@/components/KFIcon';
import ParameterInput from '@/components/ParameterInput';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
import ParameterSelect from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
@@ -25,10 +25,10 @@ import { canInput, createMenuItems } from './utils';
const { TextArea } = Input;

type PipelineNodeParameterProps = {
onParentChange: (data: PipelineNodeModelSerialize) => void;
onFormChange: (data: PipelineNodeModelSerialize) => void;
};

const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParameterProps, ref) => {
const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => {
const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize,
@@ -37,19 +37,28 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);

const afterOpenChange = () => {
const afterOpenChange = async () => {
if (!open) {
console.log('getFieldsValue', form.getFieldsValue());
const control_strategy = form.getFieldValue('control_strategy');
const in_parameters = form.getFieldValue('in_parameters');
const out_parameters = form.getFieldValue('out_parameters');
onParentChange({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_values, error] = await to(form.validateFields());
const fields = form.getFieldsValue();
const control_strategy = JSON.stringify(fields.control_strategy);
const in_parameters = JSON.stringify(fields.in_parameters);
const out_parameters = JSON.stringify(fields.out_parameters);
console.log('getFieldsValue', fields);

const res = {
...stagingItem,
...form.getFieldsValue(),
control_strategy: JSON.stringify(control_strategy),
in_parameters: JSON.stringify(in_parameters),
out_parameters: JSON.stringify(out_parameters),
});
...fields,
control_strategy: control_strategy,
in_parameters: in_parameters,
out_parameters: out_parameters,
formError: !!error,
};

console.log('res', res);

onFormChange(res);
}
};
const onClose = () => {
@@ -57,15 +66,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
};

useImperativeHandle(ref, () => ({
getFieldsValue: async () => {
const [propsRes, propsError] = await to(form.validateFields());
if (propsRes && !propsError) {
const values = form.getFieldsValue();
return values;
} else {
return Promise.reject(propsError);
}
},
showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) {
if (e.item && e.item.getModel()) {
form.resetFields();
@@ -115,7 +115,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
type = ResourceSelectorType.Mirror;
break;
}

const fieldValue = form.getFieldValue(formItemName);
const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined;
const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : [];
@@ -162,8 +161,21 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
});
}
} else {
form.setFieldValue(formItemName, '');
if (type === ResourceSelectorType.Mirror && formItemName === 'image') {
form.setFieldValue(formItemName, undefined);
} else {
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
}
}
form.validateFields([formItemName]);
close();
},
});
@@ -212,6 +224,18 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
);
};

// 必填项校验规则
const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => {
return item.value.require
? [
{
validator: requiredValidator,
message: '必填项',
},
]
: [];
};

// 控制策略
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
@@ -232,7 +256,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
<Drawer
title="编辑任务"
placement="right"
rootStyle={{ marginTop: '45px' }}
rootStyle={{ marginTop: '52px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
@@ -351,7 +375,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
value: 'standard',
}}
showSearch
allowClear
/>
</Form.Item>
<Form.Item
@@ -382,11 +405,14 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{/* 控制参数 */}
{controlStrategyList.map((item) => (
<Form.Item
key={item.key}
name={['control_strategy', item.key]}
required={item.value.require ? true : false}
label={getLabel(item, 'control_strategy')}
rules={getFormRules(item)}
>
<ParameterInput allowClear></ParameterInput>
</Form.Item>
@@ -401,11 +427,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
required={item.value.require ? true : false}
>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item
name={['in_parameters', item.key]}
noStyle
rules={[{ required: item.value.require ? true : false }]}
>
<Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle>
{item.value.type === 'select' ? (
<ParameterSelect />
) : (
@@ -435,8 +457,9 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
required={item.value.require ? true : false}
label={getLabel(item, 'out_parameters')}
rules={[{ required: item.value.require ? true : false }]}
rules={getFormRules(item)}
>
<ParameterInput allowClear></ParameterInput>
</Form.Item>


+ 23
- 0
react-ui/src/utils/index.ts View File

@@ -109,6 +109,29 @@ export function nullToUndefined(obj: Record<string, any>) {
return newObj;
}

// undefined to null
export function undefinedToNull(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value === undefined) {
newObj[key] = null;
} else if (Array.isArray(value)) {
newObj[key] = value.map((item) => undefinedToNull(item));
} else if (isPlainObject(value)) {
newObj[key] = undefinedToNull(value);
} else {
newObj[key] = value;
}
}
}
return newObj;
}

/**
* Changes the property names of an object based on a mapping provided.
*


+ 1
- 7
react-ui/src/utils/promise.ts View File

@@ -7,13 +7,7 @@ export async function to<T>(promise: Promise<T>): Promise<[T, null] | [null, Err
const data = await promise;
return [data, null];
} catch (error) {
if (error instanceof Error) {
return [null, error];
} else if (typeof error === 'string') {
return [null, new Error(error)];
} else {
return [null, new Error('Error')];
}
return [null, error as Error];
}
}



Loading…
Cancel
Save