From 5ee8a6bc17b1c2910c5b753af66e43e83e00cd95 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 2 Jul 2024 17:05:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B5=81=E6=B0=B4=E7=BA=BF=E5=BF=85?= =?UTF-8?q?=E5=A1=AB=E9=A1=B9=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/img/experiment-running.png | Bin 0 -> 2097 bytes react-ui/src/assets/img/pipeline-warning.png | Bin 0 -> 1652 bytes .../src/components/ParameterInput/index.tsx | 68 ++++++++----- react-ui/src/pages/Experiment/Info/index.jsx | 2 +- .../pages/ModelDeployment/Create/index.tsx | 37 ++++++-- .../src/pages/Pipeline/editPipeline/index.jsx | 69 ++++++++------ .../pages/Pipeline/editPipeline/index.less | 3 +- .../src/pages/Pipeline/editPipeline/props.tsx | 89 +++++++++++------- react-ui/src/utils/index.ts | 23 +++++ react-ui/src/utils/promise.ts | 8 +- 10 files changed, 195 insertions(+), 104 deletions(-) create mode 100644 react-ui/src/assets/img/experiment-running.png create mode 100644 react-ui/src/assets/img/pipeline-warning.png 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 0000000000000000000000000000000000000000..3155302a67febe5f321b51c9ae34f25244b2b12d GIT binary patch literal 2097 zcmV-12+sG3P)%f%Q9^nx#74MvUf;Ekx&2LU4iRH!#VLM-$W z#RN=*00}DKgCaa21~9aR5=B6ii%Q@@kQX3P6EP~mTR>1FyDiXat+uA53ovQb;>5NpuWrydDkFW?CmM2eDQlK$OmuqhRw5U^YAmp>%lwkY8ky z>RQ%|&voZG(fI;m@C-RL9Htdu^0Z$;{LBX+TPaR2#+t>SUio+iqNGla3`_bD3lH&4rqERBIEt&z2uv<{)r zc&@2%Ug!aYguj{Ff9}sXhnNGK@Xg57T>zbvfM zKQW_1i4JaiN`Eft=;bg~o?R!}a!qsv0uka(4(W0?Mt^hmG;g+)X0?pVUDgq^(eGoo1CJ)dqL)Ac=K}~C91qR69D@f49N12 zi9R^2ySg47z*vTm3h6RORv_3xhWpb=JS^5*f@t1jsg3feLZ)%(ht%&3z*{qj?gs#- zBWOSX#JPBwDzZ7wtU#34%Eb`+RTIIp0ikM%#f#N`&)1peQw-B$Z5ek*3Oscj1gO=k ziQ3PbNaRSn1dfyUmo_qicu+>lhtnTOpgUS4V#E|m?4mthL)v@-qNt1%e+U+JRp3oiPpzebxxg(DCHt7S!>2pu;Cf$8l) zwEdGE1kN$oUziTnOgNg1TxiN6o&HjsZaCL0PFR0UGmWyDvKqBKl-YbZnMO8FqnCOE z z49ysgAn2^STterg!(^?<+Q3@D!w(Q{QNvTgWeLgk0M`ZFJ8)~)d37VF8@guZFOTq% zS+XW@3d|#jS|A%xYWAAn%+g1#!GiBVxokC4e8%_;9JmZ;po_0bJ@Q-G=BzdRRG%!I z8rnFOW*hdCPOE6B86~&u?waqfTa~l=#Rp+&5sWP><*Vs(BXW6h% zUwj5~DPG5TbhWL)AWRCNfveTCB<^D0NWZg{=!>06X$QlKCfvH)i!PN@uEFy-PvGK8 z9Sp*=2+O{66iG3(=`!}_^j(9$X)N{w1kdHSx)BKwZ_Ym0WLQ(I4?*l`ncn^xkmqsp zchMGq&QV!^F=~t@c6qV_F{oY^M-p@bS>~1M%nvB+?D!4aKis(St37WQh(jZP+2XHZK>^^V4g5T?ui*vx=da<+wD)|30xe8rJN{c``o;^19@?-S{mCbC$>e%;h#hB9 zf{wt5Dhg?Hy0Y*Ab~wm;vN=eVhbs^rCNe|b84A%sfN`a@r7OtQq=P2>l}OUPXEunV zZgOY=(HTV^2fEvO)HzE@598dFRz9l@1cT9W23ycdH*^c+`79((YjtHY0-{4R>g02{ zBeABC&tgM!>sy*cqb>+J^N3tba3*^y&LO{VvQT%?qM&*Phd_*vL>g;^wV~eo8p^93 zP$LK9v)F2w$&~JT;|}X=x*%zGdy_chRSwS@p)UBpztfe5K5qGyCuu4i)k2_~_gj9H z#Np8hZ7Qf+es*;%sg)DM=%e88!KUVtcH-VvGfukf@@fK3^8zsqC+5mc;4z>XqkFDf z;ddMwwjYt;K+n4goa0rc-3^c6emLI|SMP>PYC(NNBJdn+6o46>${q@6Gw-N{80jOFNK*%W9U`nq8h57VH8`qNrk|>DMRM!FD)$t5-<`0fe1WWPIq>e z>63Txm7PtmJ(2}4u?m&1a1rHP6kw@6S3LFDgD8F6bp|YQ zEzklfv1Vs+T)1#Tm7dkT1=s2kY=JD%CziS4(m4Xo7`nTEffT`D3+L11tB@b%@v71)gY=Pv5%dsCLjk!Gg zvLu|k{kqb2Z6IXfvTfulknUF^fuQ$(fXc#K#>1%=5VCxN1PGTZLb7nGD-Y2E0=)sJ z1nJ`$wjaxv(k@uaa@=DR9vK9B!!?xO&hTsv94^t_Szh6RVEIRYU8gjhq5_Vath1^Q!2Nhp8p*3wG`O_B9uRLO)OOv`U3K&Zm0 ztV$*XAq$r(-Qci1!kcmtt7KddQu(d_JlI2mT0;;5wP#Fu1nWMEtk(oWYqd%O#v}+; zI8Bqku^!TRyr2n$?rN2Yg;5a9D?(Pu8bIif--6B#(;&iz(*_VSZwQ+is({2!!&NU3 z>{a%f`AYx|AVgbfQ}SDbFvHZXi$4c2FWe&{Se=k}6q!n!XQ0w1Lu5y9_gK6VKL{4eI-^7kd96AOvT!@RZ(DdttP3u`Fb8 z>iDjZqN<568mmvq)JW%w`}X^yMrH*M9G>*Q*zO<0-Q;Af@f=EH)F~kmAgFuePEb%8qgl^r%BG96DyqSCXG>Ya8Rn=x*;X4wW(CCwT z4SIuPJ?g37W;buKCC3Ak8mIC1a1<+U_9+u&j1SWA3qQhq literal 0 HcmV?d00001 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