diff --git a/react-ui/src/assets/img/experiment-running.png b/react-ui/src/assets/img/experiment-running.png new file mode 100644 index 00000000..3155302a Binary files /dev/null and b/react-ui/src/assets/img/experiment-running.png differ diff --git a/react-ui/src/assets/img/pipeline-warning.png b/react-ui/src/assets/img/pipeline-warning.png new file mode 100644 index 00000000..67b8c65c Binary files /dev/null and b/react-ui/src/assets/img/pipeline-warning.png differ diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 0fc08551..0c422bff 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -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; +// 对象 +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) => { + e.stopPropagation(); + onChange?.({ + ...valueObj, + value: undefined, + showValue: undefined, + fromSelect: false, + activeTab: undefined, + expandedKeys: [], + checkedKeys: [], + }); + onRemove?.(); + }; return ( <> @@ -62,18 +88,7 @@ function ParameterInput({ {valueObj?.showValue} { - e.stopPropagation(); - onChange?.({ - ...valueObj, - value: undefined, - showValue: undefined, - fromSelect: false, - activeTab: undefined, - expandedKeys: undefined, - checkedKeys: undefined, - }); - }} + onClick={handleRemove} /> ) : ( @@ -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(); +}; diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index 03398efb..f8325562 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -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(); diff --git a/react-ui/src/pages/ModelDeployment/Create/index.tsx b/react-ui/src/pages/ModelDeployment/Create/index.tsx index 669130e6..d9836195 100644 --- a/react-ui/src/pages/ModelDeployment/Create/index.tsx +++ b/react-ui/src/pages/ModelDeployment/Create/index.tsx @@ -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 > selectResource('model', ResourceSelectorType.Model)} + onChange={() => setSelectedModel(undefined)} /> @@ -291,16 +306,18 @@ function ModelDeploymentCreate() { name="image" rules={[ { - required: true, - message: '请输入镜像', + validator: requiredValidator, + message: '请选择镜像', }, ]} + required > selectResource('image', ResourceSelectorType.Mirror)} + onChange={() => setSelectedMirror(undefined)} /> diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx index 1fc2f62b..c67c40ed 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -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 `
-
- +
+ - 复制 + 复制
-
- +
+ - 删除 + 删除
`; }, 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 = () => {
- + 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( {} as PipelineNodeModelSerialize, @@ -37,19 +37,28 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 const [menuItems, setMenuItems] = useState([]); - 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