diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts index 3b681f86..759d7c56 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -76,7 +76,7 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: '智能软件开发平台', + title: '智能材料科研平台', layout: { locale: false, ...defaultSettings, diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index 97a26343..d1842286 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -16,7 +16,7 @@ const Settings: ProLayoutProps & { fixSiderbar: false, splitMenus: false, colorWeak: false, - title: '智能软件开发平台', + title: '智能材料科研平台', pwa: true, logo: '/assets/images/left-top-logo.png', token: { diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index e89d5d60..960a3cb0 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -112,7 +112,7 @@ export default [ { name: '开发环境', path: '', - component: './DevelopmentEnvironment/List', + component: './DevelopmentEnvironment/Editor', }, { name: '创建编辑器', diff --git a/react-ui/mock/model.ts b/react-ui/mock/model.ts index 02054802..af637db0 100644 --- a/react-ui/mock/model.ts +++ b/react-ui/mock/model.ts @@ -48,12 +48,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.1.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: null, @@ -80,12 +101,38 @@ export default defineMock({ exp_ins_id: null, version: 'v0.3.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 120, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [], @@ -110,12 +157,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.31.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [], @@ -140,12 +208,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.4.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [ @@ -154,12 +243,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.6.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [], @@ -231,12 +341,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.5.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [ @@ -275,12 +406,33 @@ export default defineMock({ exp_ins_id: null, version: 'v0.11.0', ref_item: null, - train_task: {}, - train_dataset: [], - train_params: [], - train_image: null, - test_dataset: [], - project_dependency: {}, + train_task: { + name: '模型训练测试导出0529', + ins_id: 229, + task_id: 'model-train-5d76f002', + }, + train_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + train_params: ['256', '2'], + train_image: + '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', + test_dataset: [ + { + dataset_id: 20, + dataset_version: 'v0.1.0', + dataset_name: '手写体识别模型依赖测试训练数据集', + }, + ], + project_dependency: { + url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + name: 'somun202304241505581', + branch: 'train_ci_test', + }, parent_models_map: [], parent_models: [], children_models: [], diff --git a/react-ui/public/assets/images/left-top-logo-1.png b/react-ui/public/assets/images/left-top-logo-1.png new file mode 100644 index 00000000..64bdaf2c Binary files /dev/null and b/react-ui/public/assets/images/left-top-logo-1.png differ diff --git a/react-ui/public/assets/images/left-top-logo.png b/react-ui/public/assets/images/left-top-logo.png index 64bdaf2c..e2fbcfe5 100644 Binary files a/react-ui/public/assets/images/left-top-logo.png and b/react-ui/public/assets/images/left-top-logo.png differ diff --git a/react-ui/src/assets/img/experiment-pending.png b/react-ui/src/assets/img/experiment-pending.png new file mode 100644 index 00000000..ceefa027 Binary files /dev/null and b/react-ui/src/assets/img/experiment-pending.png differ 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..d4121030 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/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index 65239957..e50dabec 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-17 12:53:06 - * @Description: + * @Description: 封装 iconfont 组件 */ import '@/iconfont/iconfont-menu.js'; import '@/iconfont/iconfont.js'; diff --git a/react-ui/src/components/KFSpin/index.less b/react-ui/src/components/KFSpin/index.less new file mode 100644 index 00000000..931e7ea0 --- /dev/null +++ b/react-ui/src/components/KFSpin/index.less @@ -0,0 +1,19 @@ +.kf-spin { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.5); + + &__label { + margin-top: 20px; + color: @text-color; + font-size: @font-size-content; + } +} diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx new file mode 100644 index 00000000..64d315a9 --- /dev/null +++ b/react-ui/src/components/KFSpin/index.tsx @@ -0,0 +1,13 @@ +import { Spin, SpinProps } from 'antd'; +import styles from './index.less'; + +function KFSpin(props: SpinProps) { + return ( +
+ +
加载中
+
+ ); +} + +export default KFSpin; diff --git a/react-ui/src/components/PageTitle/index.less b/react-ui/src/components/PageTitle/index.less index d120009b..47907246 100644 --- a/react-ui/src/components/PageTitle/index.less +++ b/react-ui/src/components/PageTitle/index.less @@ -6,5 +6,5 @@ background-image: url(@/assets/img/page-title-bg.png); background-repeat: no-repeat; background-position: top center; - background-size: 100%; + background-size: 100% 100%; } diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 2d4f0489..6426985e 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -62,3 +62,7 @@ font-size: 12px; } } + +.parameter-input.parameter-input--error { + border-color: @error-color; +} diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 0fc08551..8ce18830 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 { Form, 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; @@ -21,12 +31,14 @@ interface ParameterInputProps { style?: React.CSSProperties; size?: 'middle' | 'small' | 'large'; disabled?: boolean; + id?: string; } function ParameterInput({ value, onChange, onClick, + onRemove, canInput = true, textArea = false, allowClear, @@ -34,6 +46,7 @@ function ParameterInput({ style, size = 'middle', disabled = false, + id, ...rest }: ParameterInputProps) { const valueObj = @@ -42,16 +55,34 @@ 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 { status } = Form.Item.useStatus(); + + // 删除 + const handleRemove = (e: React.MouseEvent) => { + e.stopPropagation(); + onChange?.({ + ...valueObj, + value: undefined, + showValue: undefined, + fromSelect: false, + activeTab: undefined, + expandedKeys: [], + checkedKeys: [], + }); + onRemove?.(); + }; return ( <> {(isSelect || !canInput) && !disabled ? (
{valueObj?.showValue} { - e.stopPropagation(); - onChange?.({ - ...valueObj, - value: undefined, - showValue: undefined, - fromSelect: false, - activeTab: undefined, - expandedKeys: undefined, - checkedKeys: undefined, - }); - }} + onClick={handleRemove} />
) : ( @@ -83,6 +103,7 @@ function ParameterInput({ ) : ( onChange?.({ ...valueObj, - fromSelect: false, value: e.target.value, showValue: e.target.value, + fromSelect: false, }) } /> @@ -105,3 +126,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/components/ResourceSelect/index.less b/react-ui/src/components/ResourceSelect/index.less new file mode 100644 index 00000000..8a586c52 --- /dev/null +++ b/react-ui/src/components/ResourceSelect/index.less @@ -0,0 +1,11 @@ +.kf-resource-select { + position: relative; + display: flex; + align-items: center; + + &__button { + position: absolute; + top: 0; + left: calc(100% + 10px); + } +} diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx new file mode 100644 index 00000000..96f5e96e --- /dev/null +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -0,0 +1,104 @@ +import KFIcon from '@/components/KFIcon'; +import ResourceSelectorModal, { + ResourceSelectorResponse, + ResourceSelectorType, + selectorTypeConfig, +} from '@/pages/Pipeline/components/ResourceSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { Button } from 'antd'; +import { useState } from 'react'; +import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; +import './index.less'; + +export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; + +type ResourceSelectProps = { + type: ResourceSelectorType; +} & ParameterInputProps; + +// 获取选择数据集、模型后面按钮 icon +const getSelectBtnIcon = (type: ResourceSelectorType) => { + return ; +}; + +function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps) { + const [selectedResource, setSelectedResource] = useState( + undefined, + ); + + const selectResource = () => { + const resource = selectedResource; + const { close } = openAntdModal(ResourceSelectorModal, { + type, + defaultExpandedKeys: resource ? [resource.id] : [], + defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], + defaultActiveTab: resource?.activeTab, + onOk: (res) => { + setSelectedResource(res); + if (res) { + const { activeTab, id, name, version, path } = res; + if (type === ResourceSelectorType.Mirror) { + onChange?.({ + value: path, + showValue: path, + fromSelect: true, + activeTab, + expandedKeys: [`${id}`], + checkedKeys: [`${id}-${version}`], + }); + } else { + const jsonObj = { + id, + version, + path, + }; + const jsonObjStr = JSON.stringify(jsonObj); + const showValue = `${name}:${version}`; + onChange?.({ + value: jsonObjStr, + showValue, + fromSelect: true, + activeTab, + expandedKeys: [`${id}`], + checkedKeys: [`${id}-${version}`], + ...jsonObj, + }); + } + } else { + onChange?.({ + value: undefined, + showValue: undefined, + fromSelect: false, + activeTab: undefined, + expandedKeys: [], + checkedKeys: [], + }); + } + close(); + }, + }); + }; + + return ( +
+ setSelectedResource(undefined)} + onClick={selectResource} + > + +
+ ); +} + +export default ResourceSelect; diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index b34d5156..adf61e7d 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -32,6 +32,7 @@ export function useStateRef(initialValue: T) { */ export function useVisible(initialValue: boolean) { const [visible, setVisible] = useState(initialValue); + const ref = useRef(initialValue); const open = useCallback(() => { setVisible(true); @@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) { setVisible(false); }, []); - return [visible, open, close] as const; + useEffect(() => { + ref.current = visible; + }, [visible]); + + return [visible, open, close, ref] as const; } type Callback = (state: T) => void; diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js index 1ec213e7..6d617cfb 100644 --- a/react-ui/src/iconfont/iconfont.js +++ b/react-ui/src/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window); \ No newline at end of file +window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window); \ No newline at end of file diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 036fc12c..e59e9698 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -1,35 +1,32 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 13:58:08 - * @Description: 创建镜像 + * @Description: 创建开发环境 */ import KFIcon from '@/components/KFIcon'; import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import PageTitle from '@/components/PageTitle'; -import ParameterInput from '@/components/ParameterInput'; +import ResourceSelect, { + requiredValidator, + type ParameterInputObject, +} from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; -import ResourceSelectorModal, { - ResourceSelectorResponse, - ResourceSelectorType, - selectorTypeConfig, -} from '@/pages/Pipeline/components/ResourceSelectorModal'; +import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; import { createEditorReq } from '@/services/developmentEnvironment'; -import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { useNavigate } from '@umijs/max'; import { App, Button, Col, Form, Input, Row, Select } from 'antd'; -import { pick } from 'lodash'; -import { useState } from 'react'; +import { omit, pick } from 'lodash'; import styles from './index.less'; type FormData = { name: string; computing_resource: string; standard: string; - image: string; - model: ResourceSelectorResponse; - dataset: ResourceSelectorResponse; + image: ParameterInputObject; + model: ParameterInputObject; + dataset: ParameterInputObject; }; enum ComputingResourceType { @@ -55,25 +52,20 @@ function EditorCreate() { const [form] = Form.useForm(); const { message } = App.useApp(); const [resourceStandardList, filterResourceStandard] = useComputingResource(); - const [selectedModel, setSelectedModel] = useState( - undefined, - ); // 选择的模型,为了再次打开时恢复原来的选择 - const [selectedDataset, setSelectedDataset] = useState( - undefined, - ); // 选择的数据集,为了再次打开时恢复原来的选择 - const [selectedMirror, setSelectedMirror] = useState( - undefined, - ); // 选择的镜像,为了再次打开时恢复原来的选择 // 创建编辑器 const createEditor = async (formData: FormData) => { - // const { model, dataset } = formData; - // const params = { - // ...formData, - // model: JSON.stringify(omit(model, ['showValue'])), - // dataset: JSON.stringify(dataset, ['showValue']), - // }; - const [res] = await to(createEditorReq(formData)); + // 根据后台要求,修改表单数据 + const image = formData['image']; + const model = formData['model']; + const dataset = formData['dataset']; + const params = { + ...omit(formData, ['image', 'model', 'dataset']), + image: image.value, + model: pick(model, ['id', 'version', 'path', 'showValue']), + dataset: pick(dataset, ['id', 'version', 'path', 'showValue']), + }; + const [res] = await to(createEditorReq(params)); if (res) { message.success('创建成功'); navgite(-1); @@ -89,61 +81,6 @@ function EditorCreate() { const cancel = () => { navgite(-1); }; - // 获取选择数据集、模型后面按钮 icon - const getSelectBtnIcon = (type: ResourceSelectorType) => { - return ; - }; - - // 选择模型、镜像、数据集 - const selectResource = (name: string, type: ResourceSelectorType) => { - let resource: ResourceSelectorResponse | undefined; - switch (type) { - case ResourceSelectorType.Model: - resource = selectedModel; - break; - case ResourceSelectorType.Dataset: - resource = selectedDataset; - break; - default: - resource = selectedMirror; - break; - } - const { close } = openAntdModal(ResourceSelectorModal, { - type, - defaultExpandedKeys: resource ? [resource.id] : [], - defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], - defaultActiveTab: resource?.activeTab, - onOk: (res) => { - if (res) { - if (type === ResourceSelectorType.Mirror) { - form.setFieldValue(name, res.path); - setSelectedMirror(res); - } else { - const showValue = `${res.name}:${res.version}`; - form.setFieldValue(name, { - ...pick(res, ['id', 'version', 'path']), - showValue, - }); - if (type === ResourceSelectorType.Model) { - setSelectedModel(res); - } else if (type === ResourceSelectorType.Dataset) { - setSelectedDataset(res); - } - } - } else { - if (type === ResourceSelectorType.Model) { - setSelectedModel(undefined); - } else if (type === ResourceSelectorType.Dataset) { - setSelectedDataset(undefined); - } else if (type === ResourceSelectorType.Mirror) { - setSelectedMirror(undefined); - } - form.setFieldValue(name, ''); - } - close(); - }, - }); - }; return (
@@ -230,64 +167,46 @@ function EditorCreate() { - selectResource('image', ResourceSelectorType.Mirror)} /> - - - - selectResource('model', ResourceSelectorType.Model)} /> - - - @@ -296,29 +215,20 @@ function EditorCreate() { name="dataset" rules={[ { - required: true, + validator: requiredValidator, message: '请选择数据集', }, ]} + required > - selectResource('dataset', ResourceSelectorType.Dataset)} /> - - - diff --git a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx index c7b22c6a..38a98e6a 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 13:58:08 - * @Description: 开发环境 + * @Description: 开发环境列表 */ import CommonTableCell from '@/components/CommonTableCell'; import DateTableCell from '@/components/DateTableCell'; diff --git a/react-ui/src/pages/Experiment/Comparison/config.tsx b/react-ui/src/pages/Experiment/Comparison/config.tsx new file mode 100644 index 00000000..c6c53971 --- /dev/null +++ b/react-ui/src/pages/Experiment/Comparison/config.tsx @@ -0,0 +1,17 @@ +export enum ComparisonType { + Train = 'Train', // 训练 + Evaluate = 'Evaluate', // 评估 +} + +type ComparisonTypeInfo = { + title: string; +}; + +export const comparisonConfig: Record = { + [ComparisonType.Train]: { + title: '训练', + }, + [ComparisonType.Evaluate]: { + title: '评估', + }, +}; diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index a491c621..7a97a588 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -22,6 +22,12 @@ .ant-table-container { border: none !important; } + .ant-table-thead { + .ant-table-cell { + background-color: rgb(247, 247, 247); + border-color: #e8e8e8 !important; + } + } .ant-table-tbody { .ant-table-cell { border-right: none !important; diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 59cd3f8b..3aca2028 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -7,17 +7,13 @@ import { import { to } from '@/utils/promise'; import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; import { useSearchParams } from '@umijs/max'; -import { App, Button, Table, /*TablePaginationConfig,*/ TableProps } from 'antd'; +import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import ExperimentStatusCell from '../components/ExperimentStatusCell'; +import { ComparisonType, comparisonConfig } from './config'; import styles from './index.less'; -export enum ComparisonType { - Train = 'Train', // 训练 - Evaluate = 'Evaluate', // 评估 -} - type TableData = { experiment_ins_id: number; run_id: string; @@ -32,13 +28,15 @@ type TableData = { function ExperimentComparison() { const [searchParams] = useSearchParams(); - const comparisonType = searchParams.get('type'); + const comparisonType = searchParams.get('type') as ComparisonType; const experimentId = searchParams.get('id'); const [tableData, setTableData] = useState([]); // const [cacheState, setCacheState] = useCacheState(); // const [total, setTotal] = useState(0); const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [loading, setLoading] = useState(false); const { message } = App.useApp(); + const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); // const [pagination, setPagination] = useState( // cacheState?.pagination ?? { // current: 1, @@ -52,9 +50,11 @@ function ExperimentComparison() { // 获取对比数据列表 const getComparisonData = async () => { + setLoading(true); const request = comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; const [res] = await to(request(experimentId)); + setLoading(false); if (res && res.data) { // const { content = [], totalElements = 0 } = res.data; setTableData(res.data); @@ -91,6 +91,7 @@ function ExperimentComparison() { // 选择行 const rowSelection: TableProps['rowSelection'] = { type: 'checkbox', + fixed: 'left', selectedRowKeys, onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); @@ -108,7 +109,9 @@ function ExperimentComparison() { title: '实例 ID', dataIndex: 'experiment_ins_id', key: 'experiment_ins_id', - width: '20%', + width: 100, + fixed: 'left', + align: 'center', render: tableCellRender(), }, { @@ -116,43 +119,61 @@ function ExperimentComparison() { dataIndex: 'start_time', key: 'start_time', width: 180, + fixed: 'left', + align: 'center', render: tableCellRender(false, dateFormatter), }, { title: '运行状态', dataIndex: 'status', key: 'status', - width: '20%', + width: 100, + fixed: 'left', + align: 'center', render: ExperimentStatusCell, }, { - title: '训练数据集', + title: `${config.title}数据集`, dataIndex: 'dataset', key: 'dataset', - width: '20%', + width: 180, + fixed: 'left', + align: 'center', render: tableCellRender(true, arrayFormatter()), ellipsis: { showTitle: false }, }, ], }, { - title: '训练参数', + title: `${config.title}参数`, + align: 'center', children: first?.params_names.map((name) => ({ - title: name, + title: ( + + {name} + + ), dataIndex: ['params', name], key: name, - width: '20%', + width: 120, + align: 'center', render: tableCellRender(true), ellipsis: { showTitle: false }, })), }, { - title: '训练指标', + title: `${config.title}指标`, + align: 'center', children: first?.metrics_names.map((name) => ({ - title: name, + title: ( + + {name} + + ), dataIndex: ['metrics', name], key: name, - width: '20%', + width: 120, + align: 'center', render: tableCellRender(true), ellipsis: { showTitle: false }, })), @@ -177,9 +198,10 @@ function ExperimentComparison() { dataSource={tableData} columns={columns} rowSelection={rowSelection} - scroll={{ y: 'calc(100% - 55px)' }} + scroll={{ y: 'calc(100% - 55px)', x: '100%' }} pagination={false} bordered={true} + loading={loading} // pagination={{ // ...pagination, // total: total, diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index db217197..3c1eda2f 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -1,76 +1,158 @@ +import { ExperimentStatus } from '@/enums'; import { useStateRef, useVisible } from '@/hooks'; import { getExperimentIns } from '@/services/experiment/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js'; import themes from '@/styles/theme.less'; import { fittingString } from '@/utils'; import { elapsedTime, formatDate } from '@/utils/date'; -import G6 from '@antv/g6'; +import { to } from '@/utils/promise'; +import G6, { Util } from '@antv/g6'; import { Button } from 'antd'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import ParamsModal from '../components/ViewParamsModal'; import { experimentStatusInfo } from '../status'; import styles from './index.less'; -import Props from './props'; +import ExperimentDrawer from './props'; let graph = null; function ExperimentText() { - const [message, setMessage, messageRef] = useStateRef({}); - const propsRef = useRef(); - const navgite = useNavigate(); - const locationParams = useParams(); //新版本获取路由参数接口 - const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); + const [experimentIns, setExperimentIns] = useState(undefined); + const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); const graphRef = useRef(); + const timerRef = useRef(); + const workflowRef = useRef(); + const locationParams = useParams(); // 新版本获取路由参数接口 + const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); + const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = + useVisible(false); + const navigate = useNavigate(); + const width = 110; + const height = 36; - const getGraphData = (data) => { - if (graph) { - // 修改历史数据有蓝色边框的问题 - data.nodes.forEach((item) => { - item.style.stroke = '#fff'; + useEffect(() => { + initGraph(); + getWorkflow(); + + const changeSize = () => { + if (!graph || graph.get('destroyed')) return; + if (!graphRef.current) return; + graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); + graph.fitView(); + }; + + window.addEventListener('resize', changeSize); + return () => { + window.removeEventListener('resize', changeSize); + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); + + useEffect(() => { + propsDrawerOpenRef.current = propsDrawerOpen; + }, [propsDrawerOpen]); + + // 获取流水线模版 + const getWorkflow = async () => { + const [res] = await to(getWorkflowById(locationParams.workflowId)); + if (res && res.data && res.data.dag) { + try { + const dag = JSON.parse(res.data.dag); + dag.nodes.forEach((item) => { + item.in_parameters = JSON.parse(item.in_parameters); + item.out_parameters = JSON.parse(item.out_parameters); + item.control_strategy = JSON.parse(item.control_strategy); + item.imgName = item.img.slice(0, item.img.length - 4); + }); + workflowRef.current = dag; + getExperimentInstance(true); + } catch (error) { + // JSON.parse 错误 + console.log(error); + } + } + }; + + // 获取实验实例 + const getExperimentInstance = async (first) => { + const [res] = await to(getExperimentIns(locationParams.id)); + if (res && res.data && workflowRef.current) { + setExperimentIns(res.data); + const { status, nodes_status } = res.data; + const workflowData = workflowRef.current; + const experimentStatusObjs = JSON.parse(nodes_status); + workflowData.nodes.forEach((item) => { + const experimentNode = experimentStatusObjs?.[item.id] ?? {}; + const { finishedAt, startedAt, phase, id } = experimentNode; + item.experimentStartTime = startedAt; + item.experimentEndTime = finishedAt; + item.experimentStatus = phase; + item.workflowId = id; + item.img = phase ? `${item.imgName}-${phase}.png` : `${item.imgName}.png`; }); + + // 更新打开的抽屉数据 + if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { + const currentId = experimentNodeDataRef.current.id; + const node = workflowData.nodes.find((item) => item.id === currentId); + if (node) { + setExperimentNodeData(node); + } + } + + getGraphData(workflowData, first); + + // 运行中或者等待中,每5秒获取一次实验实例 + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + timerRef.current = setTimeout(() => { + getExperimentInstance(false); + }, 5 * 1000); + } + + if (first && status === ExperimentStatus.Pending) { + const node = workflowData.nodes[0]; + if (node) { + setExperimentNodeData(node); + openPropsDrawer(); + } + } else if (first && status === ExperimentStatus.Running) { + const node = + workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? + workflowData.nodes[0]; + if (node) { + setExperimentNodeData(node); + openPropsDrawer(); + } + } + } + }; + + // 根据数据,渲染图 + const getGraphData = (data, first) => { + if (graph) { + const zoom = graph.getZoom(); + // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置 + const lastPoint = graph.getCanvasByPoint(0, 0); graph.data(data); graph.render(); + if (first) { + graph.fitView(); + } else { + graph.zoomTo(zoom); + // 获取重新渲染之后点(0, 0)在画布的位置 + const newPoint = graph.getCanvasByPoint(0, 0); + // 移动画布相对位移; + graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); + } } else { setTimeout(() => { getGraphData(data); }, 500); } }; - const getFirstWorkflow = (val) => { - getWorkflowById(val).then((pipelineRes) => { - if (graph && pipelineRes.data && pipelineRes.data.dag) { - getExperimentIns(locationParams.id).then((experimentRes) => { - if (experimentRes.code === 200) { - setMessage(experimentRes.data); - const experimentStatusObjs = JSON.parse(experimentRes.data.nodes_status); - const newNodeList = JSON.parse(pipelineRes.data.dag).nodes.map((item) => { - return { - ...item, - experimentEndTime: experimentStatusObjs?.[item.id]?.finishedAt, - experimentStartTime: experimentStatusObjs?.[item.id]?.startedAt, - experimentStatus: experimentStatusObjs?.[item.id]?.phase, - component_id: experimentStatusObjs?.[item.id]?.id, - img: experimentStatusObjs?.[item.id]?.phase - ? item.img.slice(0, item.img.length - 4) + - '-' + - experimentStatusObjs[item.id].phase + - '.png' - : item.img, - }; - }); - const newData = { ...JSON.parse(pipelineRes.data.dag), nodes: newNodeList }; - getGraphData(newData); - } - }); - } - }); - }; - - useEffect(() => { - initGraph(); - getFirstWorkflow(locationParams.workflowId); - }, []); const initGraph = () => { G6.registerNode( @@ -116,6 +198,54 @@ function ExperimentText() { draggable: true, }); } + const hasRightImg = + cfg.experimentStatus === ExperimentStatus.Pending || + cfg.experimentStatus === ExperimentStatus.Running; + if (hasRightImg) { + const image = group.addShape('image', { + attrs: { + x: -10, + y: -10, + width: 20, + height: 20, + img: + cfg.experimentStatus === ExperimentStatus.Pending + ? require('@/assets/img/experiment-pending.png') + : require('@/assets/img/experiment-running.png'), + cursor: 'pointer', + }, + draggable: false, + capture: false, + }); + + if (cfg.experimentStatus === ExperimentStatus.Running) { + image.animate( + (ratio) => { + const toMatrix = Util.transform( + [1, 0, 0, 0, 1, 0, 0, 0, 1], + [ + ['r', ratio * Math.PI * 2], + ['t', width / 2 - 14 + 10, -height / 2 - 6 + 10], + ], + ); + return { + matrix: toMatrix, + }; + }, + { + repeat: true, // 动画重复 + duration: 1000, + easing: 'easeLinear', + }, + ); + } else if (cfg.experimentStatus === ExperimentStatus.Pending) { + const toMatrix = Util.transform( + [1, 0, 0, 0, 1, 0, 0, 0, 1], + [['t', width / 2 - 14 + 10, -height / 2 - 6 + 10]], + ); + image.setMatrix(toMatrix); + } + } const bbox = group.getBBox(); const anchorPoints = this.getAnchorPoints(cfg); anchorPoints.forEach((anchorPos, i) => { @@ -139,12 +269,12 @@ function ExperimentText() { // response the state changes and show/hide the link-point circles setState(name, value, item) { const group = item.getContainer(); - const shape = group.get('children')[0]; + const shape = group.get('children')?.[0]; if (name === 'hover') { if (value) { - shape.attr('stroke', themes['primaryColor']); + shape?.attr('stroke', themes['primaryColor']); } else { - shape.attr('stroke', '#fff'); + shape?.attr('stroke', 'transparent'); } } }, @@ -181,7 +311,7 @@ function ExperimentText() { defaultNode: { type: 'rect-node', - size: [110, 36], + size: [width, height], labelCfg: { style: { @@ -196,8 +326,14 @@ function ExperimentText() { }, style: { fill: '#fff', - stroke: '#fff', - radius: 10, + stroke: 'transparent', + cursor: 'pointer', + radius: 8, + shadowColor: 'rgba(75, 84, 137, 0.4)', + shadowBlur: 6, + shadowOffsetX: 0, + shadowOffsetY: 0, + overflow: 'hidden', lineWidth: 0.5, }, }, @@ -224,9 +360,28 @@ function ExperimentText() { }, }, }); + + // 修改历史数据样式问题 + graph.node((node) => { + return { + style: { + stroke: 'transparent', + radius: 8, + }, + }; + }); + + // 绑定事件 + bindEvents(); + }; + + // 绑定事件 + const bindEvents = () => { graph.on('node:click', (e) => { if (e.target.get('name') !== 'anchor-point' && e.item) { - propsRef.current.showDrawer(e, locationParams.id, messageRef.current); + const model = e.item.getModel(); + setExperimentNodeData(model); + openPropsDrawer(); } }); graph.on('node:mouseenter', (e) => { @@ -235,22 +390,17 @@ function ExperimentText() { graph.on('node:mouseleave', (e) => { graph.setItemState(e.item, 'hover', false); }); - window.onresize = () => { - if (!graph || graph.get('destroyed')) return; - if (!graphRef.current) return; - graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); - graph.fitView(); - }; }; + return (
- 启动时间:{formatDate(message.create_time)} + 启动时间:{formatDate(experimentIns?.create_time)}
执行时长: - {elapsedTime(message.create_time, message.finish_time)} + {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)}
状态: @@ -260,11 +410,11 @@ function ExperimentText() { height: '8px', borderRadius: '50%', marginRight: '6px', - backgroundColor: experimentStatusInfo[message.status]?.color, + backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, }} >
- - {experimentStatusInfo[message.status]?.label} + + {experimentStatusInfo[experimentIns?.status]?.label}
- + {experimentNodeData ? ( + + ) : null}
); diff --git a/react-ui/src/pages/Experiment/Info/index.less b/react-ui/src/pages/Experiment/Info/index.less index f2f5510d..9004ce33 100644 --- a/react-ui/src/pages/Experiment/Info/index.less +++ b/react-ui/src/pages/Experiment/Info/index.less @@ -30,4 +30,10 @@ background-image: url(/assets/images/pipeline-canvas-back.png); background-size: 100% 100%; } + + :global { + .ant-drawer-mask { + background: transparent !important; + } + } } diff --git a/react-ui/src/pages/Experiment/Info/props.less b/react-ui/src/pages/Experiment/Info/props.less index b3294d55..1d5bdc34 100644 --- a/react-ui/src/pages/Experiment/Info/props.less +++ b/react-ui/src/pages/Experiment/Info/props.less @@ -14,7 +14,12 @@ border: 1px solid #e0eaff; } .ant-tabs-content-holder { - overflow-y: auto; + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } } } } diff --git a/react-ui/src/pages/Experiment/Info/props.tsx b/react-ui/src/pages/Experiment/Info/props.tsx index 48d72c0d..5940756a 100644 --- a/react-ui/src/pages/Experiment/Info/props.tsx +++ b/react-ui/src/pages/Experiment/Info/props.tsx @@ -1,11 +1,9 @@ -import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js'; +import { ExperimentStatus } from '@/enums'; import { PipelineNodeModelSerialize } from '@/types'; import { elapsedTime, formatDate } from '@/utils/date'; -import { to } from '@/utils/promise'; import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; -import { Drawer, Form, Tabs } from 'antd'; -import dayjs from 'dayjs'; -import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Drawer, Tabs } from 'antd'; +import { forwardRef, useImperativeHandle, useMemo } from 'react'; import ExperimentParameter from '../components/ExperimentParameter'; import ExperimentResult from '../components/ExperimentResult'; import LogList from '../components/LogList'; @@ -19,154 +17,130 @@ export type ExperimentLog = { start_time?: string; // 日志开始时间 }; -const Props = forwardRef((_, ref) => { - const [form] = Form.useForm(); - const [experimentNodeData, setExperimentNodeData] = useState( - {} as PipelineNodeModelSerialize, - ); - const [experimentResults, setExperimentResults] = useState([]); - const [experimentLogList, setExperimentLogList] = useState([]); +type ExperimentDrawerProps = { + open: boolean; + onClose: () => void; + instanceId?: number; // 实验实例 id + instanceName?: string; // 实验实例 name + instanceNamespace?: string; // 实验实例 namespace + instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 + workflowId?: string; // 实验实例工作流 id + instanceNodeStatus?: ExperimentStatus; // 在定时刷新实验实例状态中,变化一两次 + instanceNodeStartTime?: string; // 在定时刷新实验实例状态中,变化一两次 + instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 +}; - const items = [ - { - key: '1', - label: '日志详情', - children: ( - - ), - icon: , - }, - { - key: '2', - label: '配置参数', - icon: , - children: , - }, +const ExperimentDrawer = forwardRef( + ( { - key: '3', - label: '输出结果', - children: , - icon: , - }, - ]; - const [open, setOpen] = useState(false); - const onClose = () => { - setOpen(false); - }; - - // 获取实验日志 - const getExperimentLog = async (params: any, start_time: number) => { - const [res] = await to(getQueryByExperimentLog(params)); - if (res && res.data) { - const { log_type, pods, log_detail } = res.data; - if (log_type === 'normal') { - const list = [ - { - ...log_detail, - log_type, - }, - ]; - setExperimentLogList(list); - } else if (log_type === 'resource') { - const list = pods.map((v: string) => ({ - log_type, - pod_name: v, - log_content: '', - start_time, - })); - setExperimentLogList(list); - } - } - }; - - // 获取实验结果 - const getExperimentResult = async (params: any) => { - const [res] = await to(getNodeResult(params)); - if (res && res.data) { - setExperimentResults(res.data); - } - }; + open, + onClose, + instanceId, + instanceName, + instanceNamespace, + instanceNodeData, + workflowId, + instanceNodeStatus, + instanceNodeStartTime, + instanceNodeEndTime, + }: ExperimentDrawerProps, + ref, + ) => { + useImperativeHandle(ref, () => ({})); - useImperativeHandle(ref, () => ({ - showDrawer(e: any, id: string, message: any) { - setOpen(true); + // 如果性能有问题,可以进一步拆解 + const items = useMemo( + () => [ + { + key: '1', + label: '日志详情', + children: ( + + ), + icon: , + }, + { + key: '2', + label: '配置参数', + icon: , + children: , + }, + { + key: '3', + label: '输出结果', + children: ( + + ), + icon: , + }, + ], + [ + instanceNodeData, + instanceId, + instanceName, + instanceNamespace, + instanceNodeStatus, + workflowId, + instanceNodeStartTime, + ], + ); - // 获取实验参数 - const model = e.item.getModel(); - try { - const nodeData = { - ...model, - in_parameters: JSON.parse(model.in_parameters), - out_parameters: JSON.parse(model.out_parameters), - control_strategy: JSON.parse(model.control_strategy), - }; - setExperimentNodeData(nodeData); - form.setFieldsValue(nodeData); - } catch (error) { - console.log(error); - } - - // 获取实验日志和实验结果 - setExperimentLogList([]); - setExperimentResults([]); - // 如果已经运行到了 - if (e.item?.getModel()?.component_id) { - const model = e.item.getModel(); - const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6; - const params = { - task_id: model.id, - component_id: model.component_id, - name: message.argo_ins_name, - namespace: message.argo_ins_ns, - start_time: start_time, - }; - getExperimentLog(params, start_time); - getExperimentResult({ id, node_id: model.id }); - } - }, - })); - return ( - -
-
- 任务名称:{experimentNodeData.label} -
-
- 执行状态: -
- - {experimentStatusInfo[experimentNodeData.experimentStatus]?.label} - -
-
- 启动时间:{formatDate(experimentNodeData.experimentStartTime)} -
-
- 耗时: - {elapsedTime( - experimentNodeData.experimentStartTime, - experimentNodeData.experimentEndTime, - )} + return ( + +
+
+ 任务名称:{instanceNodeData.label} +
+
+ 执行状态: + {instanceNodeStatus ? ( + <> +
+ + {experimentStatusInfo[instanceNodeStatus]?.label} + + + ) : ( + '--' + )} +
+
+ 启动时间:{formatDate(instanceNodeStartTime)} +
+
+ 耗时: + {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} +
-
- - - ); -}); + + + ); + }, +); -export default Props; +export default ExperimentDrawer; diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less new file mode 100644 index 00000000..650b3153 --- /dev/null +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less @@ -0,0 +1,69 @@ +.tableExpandBox { + display: flex; + align-items: center; + width: 100%; + padding: 0 0 0 33px; + color: @text-color; + font-size: 15px; + + & > div { + padding: 0 16px; + } + + .index { + width: calc((100% + 32px + 33px) / 6.25); + } + + .tensorBoard { + width: calc((100% + 32px + 33px) / 6.25); + } + + .description { + display: flex; + flex: 1; + align-items: center; + + .startTime { + .singleLine(); + } + } + + .status { + width: 200px; + } + + .operation { + width: 334px; + } +} + +.tableExpandBoxContent { + height: 45px; + background-color: #fff; + border: 1px solid #eaeaea; + + & + & { + border-top: none; + } + + .statusBox { + display: flex; + align-items: center; + width: 200px; + + .statusIcon { + visibility: hidden; + transition: all 0.2s; + } + } + .statusBox:hover .statusIcon { + visibility: visible; + } +} + +.loadMoreBox { + display: flex; + align-items: center; + justify-content: center; + margin: 16px auto 0; +} diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx new file mode 100644 index 00000000..1fb3441e --- /dev/null +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -0,0 +1,178 @@ +import KFIcon from '@/components/KFIcon'; +import { ExperimentStatus } from '@/enums'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { + deleteQueryByExperimentInsId, + putQueryByExperimentInsId, +} from '@/services/experiment/index.js'; +import themes from '@/styles/theme.less'; +import { type ExperimentInstance } from '@/types'; +import { elapsedTime, formatDate } from '@/utils/date'; +import { to } from '@/utils/promise'; +import { modalConfirm } from '@/utils/ui'; +import { DoubleRightOutlined } from '@ant-design/icons'; +import { App, Button, ConfigProvider, Tooltip } from 'antd'; +import classNames from 'classnames'; +import TensorBoardStatusCell from '../TensorBoardStatus'; +import styles from './index.less'; + +type ExperimentInstanceProps = { + experimentInList?: ExperimentInstance[]; + experimentInsTotal: number; + onClickInstance?: (instance: ExperimentInstance) => void; + onClickTensorBoard?: (instance: ExperimentInstance) => void; + onRemove?: () => void; + onTerminate?: (instance: ExperimentInstance) => void; + onLoadMore?: () => void; +}; + +function ExperimentInstanceComponent({ + experimentInList, + experimentInsTotal, + onClickInstance, + onClickTensorBoard, + onRemove, + onTerminate, + onLoadMore, +}: ExperimentInstanceProps) { + const { message } = App.useApp(); + + // 删除实验实例确认 + const handleRemove = (instance: ExperimentInstance) => { + modalConfirm({ + title: '确定删除该条实例吗?', + onOk: () => { + deleteExperimentInstance(instance.id); + }, + }); + }; + + // 删除实验实例 + const deleteExperimentInstance = async (id: number) => { + const [res] = await to(deleteQueryByExperimentInsId(id)); + if (res) { + message.success('删除成功'); + onRemove?.(); + } + }; + + // 终止实验实例 + const terminateExperimentInstance = async (instance: ExperimentInstance) => { + const [res] = await to(putQueryByExperimentInsId(instance.id)); + if (res) { + message.success('终止成功'); + onTerminate?.(instance); + } + }; + + if (!experimentInList || experimentInList.length === 0) { + return null; + } + + return ( +
+
+
序号
+
可视化
+
+
运行时长
+
开始时间
+
+
状态
+
操作
+
+ + {experimentInList.map((item, index) => ( +
+ onClickInstance?.(item)} + > + {index + 1} + +
+ {item.nodes_result?.tensorboard_log ? ( + onClickTensorBoard?.(item)} + > + ) : ( + '--' + )} +
+
+
{elapsedTime(item.create_time, item.finish_time)}
+
+ + {formatDate(item.create_time)} + +
+
+
+ + + {experimentStatusInfo[item.status as ExperimentStatus]?.label} + +
+
+ + + + +
+
+ ))} + {experimentInsTotal > experimentInList.length ? ( +
+ +
+ ) : null} +
+ ); +} + +export default ExperimentInstanceComponent; diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less index 44b590ea..c5d9824e 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less @@ -1,5 +1,7 @@ .experiment-parameter { + height: 100%; padding-top: 8px; + overflow-y: auto; &__title { display: flex; diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index 62be954f..eb516935 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; import { PipelineNodeModelSerialize } from '@/types'; -import { Form, Input, Select, type FormProps } from 'antd'; +import { Form, Input, Select } from 'antd'; import styles from './index.less'; const { TextArea } = Input; type ExperimentParameterProps = { - form: FormProps['form']; nodeData: PipelineNodeModelSerialize; }; -function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { +function ExperimentParameter({ nodeData }: ExperimentParameterProps) { const [resourceStandardList] = useComputingResource(); // 资源规模 // 控制策略 @@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { wrapperCol={{ span: 24, }} - form={form} + initialValues={nodeData} style={{ maxWidth: 600, }} diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less index 078fe4f2..78684d72 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less @@ -1,5 +1,7 @@ .experiment-result { + height: 100%; padding: 8px; + overflow-y: auto; color: @text-color; font-size: 14px; diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx index e3cbd4da..feabcc3d 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx @@ -1,11 +1,15 @@ +import { getNodeResult } from '@/services/experiment/index.js'; import { downLoadZip } from '@/utils/downloadfile'; import { openAntdModal } from '@/utils/modal'; +import { to } from '@/utils/promise'; import { App, Button } from 'antd'; +import { useEffect, useState } from 'react'; import ExportModelModal from '../ExportModelModal'; import styles from './index.less'; type ExperimentResultProps = { - results?: ExperimentResultData[] | null; + experimentInsId?: number; // 实验实例 id + pipelineNodeId?: string; // 流水线节点 id }; type ExperimentResultData = { @@ -18,8 +22,21 @@ type ExperimentResultData = { }[]; }; -function ExperimentResult({ results }: ExperimentResultProps) { +function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { const { message } = App.useApp(); + const [experimentResults, setExperimentResults] = useState([]); + + useEffect(() => { + getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId }); + }, []); + + // 获取实验结果 + const getExperimentResult = async (params: any) => { + const [res] = await to(getNodeResult(params)); + if (res && res.data) { + setExperimentResults(res.data); + } + }; // 下载 const download = (path: string) => { @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { return (
- {results && results.length > 0 ? ( - results.map((item) => ( -
+ {experimentResults.length > 0 ? ( + experimentResults.map((item) => ( +
{item.name} @@ -427,130 +478,16 @@ function Experiment() { scroll={{ y: 'calc(100% - 55px)' }} expandable={{ expandedRowRender: (record) => ( -
- {experimentInList && experimentInList.length > 0 ? ( -
-
序号
-
可视化
-
-
运行时长
-
开始时间
-
-
状态
-
操作
-
- ) : ( - '' - )} - - {experimentInList && experimentInList.length > 0 - ? experimentInList.map((item, index) => ( -
- routerToText(e, item, record)} - > - {index + 1} - -
- {item.nodes_result?.tensorboard_log ? ( - handleTensorboard(item)} - > - ) : ( - '--' - )} -
-
-
- {elapsedTime(item.create_time, item.finish_time)} -
-
- - {formatDate(item.create_time)} - -
-
-
- - - {experimentStatusInfo[item.status]?.label} - -
-
- - - - -
-
- )) - : ''} -
+ gotoInstanceInfo(item, record)} + onClickTensorBoard={handleTensorboard} + onRemove={() => refreshExperimentIns(record.id)} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > ), - onExpand: (e, a) => { expandChange(e, a); }, diff --git a/react-ui/src/pages/Experiment/index.less b/react-ui/src/pages/Experiment/index.less index 3d991c4e..526ff59b 100644 --- a/react-ui/src/pages/Experiment/index.less +++ b/react-ui/src/pages/Experiment/index.less @@ -1,92 +1,18 @@ -.experimentTopBox { - display: flex; - align-items: center; - justify-content: flex-end; - width: 100%; - height: 49px; - padding-right: 30px; - background-image: url(/assets/images/pipeline-back.png); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 100%; -} -.pipelineTopBox { - display: flex; - align-items: center; - justify-content: flex-end; - width: 100%; - height: 49px; - margin-bottom: 10px; - padding-right: 30px; - background-image: url(/assets/images/pipeline-back.png); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 100%; -} -.tableExpandBox { - display: flex; - align-items: center; - width: 100%; - padding: 0 0 0 33px; - color: @text-color; - font-size: 15px; - - & > div { - padding: 0 16px; - } - - .index { - width: calc((100% + 32px + 33px) / 6.25); - } - - .tensorBoard { - width: calc((100% + 32px + 33px) / 6.25); - } - - .description { +.experimentBox { + height: 100%; + .experimentTopBox { display: flex; - flex: 1; align-items: center; - - .startTime { - .singleLine(); - } - } - - .status { - width: 200px; + justify-content: flex-end; + width: 100%; + height: 49px; + margin-bottom: 10px; + padding-right: 30px; + background-image: url(/assets/images/pipeline-back.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; } - - .operation { - width: 334px; - } -} -.tableExpandBoxContent { - height: 45px; - background-color: #fff; - border: 1px solid #eaeaea; - - & + & { - border-top: none; - } -} - -.statusBox { - display: flex; - align-items: center; - width: 200px; - - .statusIcon { - visibility: hidden; - transition: all 0.2s; - } -} -.statusBox:hover .statusIcon { - visibility: visible; -} - -.experimentBox { - height: 100%; .experimentTable { height: calc(100% - 60px); :global { diff --git a/react-ui/src/pages/Mirror/Info/index.tsx b/react-ui/src/pages/Mirror/Info/index.tsx index 60e19ab0..99448356 100644 --- a/react-ui/src/pages/Mirror/Info/index.tsx +++ b/react-ui/src/pages/Mirror/Info/index.tsx @@ -288,7 +288,13 @@ function MirrorInfo() { dataSource={tableData} columns={columns} scroll={{ y: 'calc(100% - 55px)' }} - pagination={{ ...pagination, total, showSizeChanger: true, showQuickJumper: true }} + pagination={{ + ...pagination, + total, + showSizeChanger: true, + showQuickJumper: true, + showTotal: () => `共${total}条`, + }} onChange={handleTableChange} rowKey="id" /> diff --git a/react-ui/src/pages/Mirror/List/index.tsx b/react-ui/src/pages/Mirror/List/index.tsx index f0b2bf77..50e2730e 100644 --- a/react-ui/src/pages/Mirror/List/index.tsx +++ b/react-ui/src/pages/Mirror/List/index.tsx @@ -241,7 +241,7 @@ function MirrorList() {
setInputText(e.target.value)} @@ -277,6 +277,7 @@ function MirrorList() { total: total, showSizeChanger: true, showQuickJumper: true, + showTotal: () => `共${total}条`, }} onChange={handleTableChange} rowKey="id" diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index f46d77ec..a0e9beaf 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -1,9 +1,15 @@ +/* + * @Author: 赵伟 + * @Date: 2024-06-07 11:24:10 + * @Description: 模型演化 + */ + import { useEffectWhen } from '@/hooks'; import { ResourceVersionData } from '@/pages/Dataset/config'; import { getModelAtlasReq } from '@/services/dataset/index.js'; import themes from '@/styles/theme.less'; import { to } from '@/utils/promise'; -import G6, { G6GraphEvent, Graph } from '@antv/g6'; +import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; // @ts-ignore import { Flex, Select } from 'antd'; import { useEffect, useRef, useState } from 'react'; @@ -11,7 +17,15 @@ import GraphLegend from '../GraphLegend'; import NodeTooltips from '../NodeTooltips'; import styles from './index.less'; import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils'; -import { getGraphData, nodeFontSize, nodeHeight, nodeWidth, normalizeTreeData } from './utils'; +import { + NodeType, + getGraphData, + nodeFontSize, + nodeHeight, + nodeWidth, + normalizeTreeData, + traverseHierarchically, +} from './utils'; type modeModelEvolutionProps = { resourceId: number; @@ -37,6 +51,8 @@ function ModelEvolution({ const [hoverNodeData, setHoverNodeData] = useState< ModelDepsData | ProjectDependency | TrainDataset | undefined >(undefined); + const apiData = useRef(undefined); // 接口返回的树形结构 + const hierarchyNodes = useRef([]); // 层级迭代树形结构,得到的节点列表 useEffect(() => { initGraph(); @@ -111,18 +127,7 @@ function ModelEvolution({ }, }, modes: { - default: [ - 'drag-canvas', - 'zoom-canvas', - // { - // type: 'collapse-expand', - // onChange(item?: Item, collapsed?: boolean) { - // const data = item!.getModel(); - // data.collapsed = collapsed; - // return true; - // }, - // }, - ], + default: ['drag-canvas', 'zoom-canvas'], }, }); @@ -161,11 +166,26 @@ function ModelEvolution({ }); graph.on('node:click', (e: G6GraphEvent) => { - const nodeItem = e.item; + const nodeItem = e.item as INode; const model = nodeItem.getModel() as ModelDepsData | ProjectDependency | TrainDataset; const { model_type } = model; - switch (model_type) { + if ( + model_type === NodeType.Project || + model_type === NodeType.TrainDataset || + model_type === NodeType.TestDataset || + !apiData.current || + !hierarchyNodes.current + ) { + return; } + + setShowNodeTooltip(false); + setEnterTooltip(false); + toggleExpended(model.id); + const graphData = getGraphData(apiData.current, hierarchyNodes.current); + graph.data(graphData); + graph.render(); + graph.fitView(); }); // 鼠标滚轮缩放时,隐藏 tooltip @@ -175,6 +195,17 @@ function ModelEvolution({ }); }; + // toggle 展开 + const toggleExpended = (id: string) => { + const nodes = hierarchyNodes.current; + for (const node of nodes) { + if (node.id === id) { + node.expanded = !node.expanded; + break; + } + } + }; + const handleTooltipsMouseEnter = () => { setEnterTooltip(true); }; @@ -192,7 +223,9 @@ function ModelEvolution({ const [res] = await to(getModelAtlasReq(params)); if (res && res.data) { const data = normalizeTreeData(res.data); - const graphData = getGraphData(data); + apiData.current = data; + hierarchyNodes.current = traverseHierarchically(data); + const graphData = getGraphData(data, hierarchyNodes.current); graph.data(graphData); graph.render(); diff --git a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx index a878321a..30a06817 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx @@ -6,21 +6,22 @@ import Hierarchy from '@antv/hierarchy'; export const nodeWidth = 90; export const nodeHeight = 40; export const vGap = nodeHeight + 20; -export const hGap = nodeWidth; +export const hGap = nodeHeight + 20; export const ellipseWidth = nodeWidth; export const labelPadding = 30; export const nodeFontSize = 8; +export const datasetHGap = 20; // 数据集节点 const datasetNodes: NodeConfig[] = []; export enum NodeType { - current = 'current', - parent = 'parent', - children = 'children', - project = 'project', - trainDataset = 'trainDataset', - testDataset = 'testDataset', + Current = 'Current', // 当前模型 + Parent = 'Parent', // 父模型 + Children = 'Children', // 子模型 + Project = 'Project', // 项目 + TrainDataset = 'TrainDataset', // 训练数据集 + TestDataset = 'TestDataset', // 测试数据集 } export type Rect = { @@ -40,14 +41,14 @@ export interface TrainDataset extends NodeConfig { dataset_id: number; dataset_name: string; dataset_version: string; - model_type: NodeType.testDataset | NodeType.trainDataset; + model_type: NodeType.TestDataset | NodeType.TrainDataset; } export interface ProjectDependency extends NodeConfig { url: string; name: string; branch: string; - model_type: NodeType.project; + model_type: NodeType.Project; } export type ModalDetail = { @@ -66,9 +67,9 @@ export interface ModelDepsAPIData { version: string; workflow_id: number; exp_ins_id: number; - model_type: NodeType.children | NodeType.current | NodeType.parent; + model_type: NodeType.Children | NodeType.Current | NodeType.Parent; current_model_name: string; - project_dependency: ProjectDependency; + project_dependency?: ProjectDependency; test_dataset: TrainDataset[]; train_dataset: TrainDataset[]; train_task: TrainTask; @@ -79,16 +80,22 @@ export interface ModelDepsAPIData { export interface ModelDepsData extends Omit, TreeGraphData { children: ModelDepsData[]; + expanded: boolean; // 是否展开 + level: number; // 层级,从 0 开始 + datasetLen: number; // 数据集数量 } // 规范化子数据 export function normalizeChildren(data: ModelDepsData[]) { if (Array.isArray(data)) { data.forEach((item) => { - item.model_type = NodeType.children; + item.model_type = NodeType.Children; + item.expanded = false; + item.level = 0; + item.datasetLen = item.train_dataset.length + item.test_dataset.length; item.id = `$M_${item.current_model_id}_${item.version}`; item.label = getLabel(item); - item.style = getStyle(NodeType.children); + item.style = getStyle(NodeType.Children); normalizeChildren(item.children); }); } @@ -111,22 +118,22 @@ export function getLabel(node: ModelDepsData | ModelDepsAPIData) { export function getStyle(model_type: NodeType) { let fill = ''; switch (model_type) { - case NodeType.current: + case NodeType.Current: fill = 'l(0) 0:#72a1ff 1:#1664ff'; break; - case NodeType.parent: + case NodeType.Parent: fill = 'l(0) 0:#93dfd1 1:#43c9b1'; break; - case NodeType.children: + case NodeType.Children: fill = 'l(0) 0:#72b4ff 1:#169aff'; break; - case NodeType.project: + case NodeType.Project: fill = 'l(0) 0:#b3a9ff 1:#8981ff'; break; - case NodeType.trainDataset: + case NodeType.TrainDataset: fill = '#a5d878'; break; - case NodeType.testDataset: + case NodeType.TestDataset: fill = '#d8b578'; break; default: @@ -145,11 +152,15 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { }) as ModelDepsData; // 设置当前模型的数据 - normalizedData.model_type = NodeType.current; + normalizedData.model_type = NodeType.Current; normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`; normalizedData.label = getLabel(normalizedData); - normalizedData.style = getStyle(NodeType.current); + normalizedData.style = getStyle(NodeType.Current); + normalizedData.expanded = true; + normalizedData.datasetLen = + normalizedData.train_dataset.length + normalizedData.test_dataset.length; normalizeChildren(normalizedData.children as ModelDepsData[]); + normalizedData.level = 0; // 将 parent_models 转换成树形结构 let parent_models = normalizedData.parent_models || []; @@ -157,10 +168,13 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { const parent = parent_models[0]; normalizedData = { ...parent, - model_type: NodeType.parent, + expanded: false, + level: 0, + datasetLen: parent.train_dataset.length + parent.test_dataset.length, + model_type: NodeType.Parent, id: `$M_${parent.current_model_id}_${parent.version}`, label: getLabel(parent), - style: getStyle(NodeType.parent), + style: getStyle(NodeType.Parent), children: [ { ...normalizedData, @@ -174,13 +188,34 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { } // 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据 -export function getGraphData(data: ModelDepsData): GraphData { +export function getGraphData(data: ModelDepsData, hierarchyNodes: ModelDepsData[]): GraphData { const config = { direction: 'LR', getHeight: () => nodeHeight, getWidth: () => nodeWidth, - getVGap: () => vGap / 2, - getHGap: () => hGap / 2, + getVGap: (node: NodeConfig) => { + const model = node as ModelDepsData; + const { model_type, expanded, project_dependency } = model; + if (model_type === NodeType.Current || model_type === NodeType.Parent) { + return vGap / 2; + } + const selfGap = expanded && project_dependency?.url ? nodeHeight + vGap : 0; + const nextNode = getSameHierarchyNextNode(model, hierarchyNodes); + if (!nextNode) { + return vGap / 2; + } + const nextGap = nextNode.expanded === true && nextNode.datasetLen > 0 ? nodeHeight + vGap : 0; + return (selfGap + nextGap + vGap) / 2; + }, + getHGap: (node: NodeConfig) => { + const model = node as ModelDepsData; + return ( + (getHierarchyWidth(model.level, hierarchyNodes) + + getHierarchyWidth(model.level + 1, hierarchyNodes) + + hGap) / + 2 + ); + }, }; // 树形布局计算出坐标 @@ -191,11 +226,11 @@ export function getGraphData(data: ModelDepsData): GraphData { Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => { const data = node.data as ModelDepsData; // 当前模型显示数据集和项目 - if (data.model_type === NodeType.current) { + if (data.expanded === true) { addDatasetDependency(data, node, nodes, edges); addProjectDependency(data, node, nodes, edges); - } else if (data.model_type === NodeType.children) { - adjustDatasetPosition(node); + } else if (data.model_type === NodeType.Children) { + // adjustDatasetPosition(node); } nodes.push({ ...data, @@ -219,16 +254,16 @@ const addDatasetDependency = ( nodes: NodeConfig[], edges: EdgeConfig[], ) => { - const { train_dataset, test_dataset } = data; + const { train_dataset, test_dataset, id } = data; train_dataset.forEach((item) => { - item.id = `$DTrain_${item.dataset_id}_${item.dataset_version}`; - item.model_type = NodeType.trainDataset; - item.style = getStyle(NodeType.trainDataset); + item.id = `$DTrain_${id}_${item.dataset_id}_${item.dataset_version}`; + item.model_type = NodeType.TrainDataset; + item.style = getStyle(NodeType.TrainDataset); }); test_dataset.forEach((item) => { - item.id = `$DTest_${item.dataset_id}_${item.dataset_version}`; - item.model_type = NodeType.testDataset; - item.style = getStyle(NodeType.testDataset); + item.id = `$DTest_${id}_${item.dataset_id}_${item.dataset_version}`; + item.model_type = NodeType.TestDataset; + item.style = getStyle(NodeType.TestDataset); }); datasetNodes.length = 0; @@ -243,7 +278,7 @@ const addDatasetDependency = ( fittingString(node.dataset_version, ellipseWidth - labelPadding, nodeFontSize); const half = len / 2 - 0.5; - node.x = currentNode.x! - (half - index) * (ellipseWidth + 20); + node.x = currentNode.x! - (half - index) * (ellipseWidth + datasetHGap); node.y = currentNode.y! - nodeHeight - vGap; nodes.push(node); datasetNodes.push(node); @@ -264,14 +299,14 @@ const addProjectDependency = ( nodes: NodeConfig[], edges: EdgeConfig[], ) => { - const { project_dependency } = data; + const { project_dependency, id } = data; if (project_dependency?.url) { const node = { ...project_dependency }; - node.id = `$P_${node.url}_${node.branch}`; - node.model_type = NodeType.project; + node.id = `$P_${id}_${node.url}_${node.branch}`; + node.model_type = NodeType.Project; node.type = 'rect'; node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize); - node.style = getStyle(NodeType.project); + node.style = getStyle(NodeType.Project); node.style.radius = nodeHeight / 2; node.x = currentNode.x; node.y = currentNode.y! + nodeHeight + vGap; @@ -331,3 +366,49 @@ function adjustDatasetPosition(node: NodeConfig) { }); } } + +// 层级遍历树结构 +export function traverseHierarchically(data: ModelDepsData | undefined): ModelDepsData[] { + if (!data) return []; + let level = 0; + data.level = level; + const result: ModelDepsData[] = [data]; + let index = 0; + + while (index < result.length) { + const item = result[index]; + if (item.children) { + item.children.forEach((child) => { + child.level = item.level + 1; + result.push(child); + }); + } + index++; + } + + return result; +} + +// 找到同层次的下一个节点 +export function getSameHierarchyNextNode(node: ModelDepsData, nodes: ModelDepsData[]) { + const index = nodes.findIndex((item) => item.id === node.id); + if (index >= 0 && index < nodes.length - 1) { + const nextNode = nodes[index + 1]; + if (nextNode.level === node.level) { + return nextNode; + } + } + return null; +} + +// 得到层级的宽度 +export function getHierarchyWidth(level: number, nodes: ModelDepsData[]) { + const hierarchyNodes = nodes + .filter((item) => item.level === level && item.expanded === true) + .sort((a, b) => b.datasetLen - a.datasetLen); + const first = hierarchyNodes[0]; + if (first) { + return Math.max(((first.datasetLen - 1) * (nodeWidth + datasetHGap)) / 2, 0); + } + return 0; +} diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx index 217222da..f5bb2c82 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx @@ -22,7 +22,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { }; const gotoModelPage = () => { - if (data.model_type === NodeType.current) { + if (data.model_type === NodeType.Current) { return; } if (data.current_model_id === resourceId) { @@ -39,7 +39,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
模型名称: - {data.model_type === NodeType.current ? ( + {data.model_type === NodeType.Current ? ( {data.model_version_dependcy_vo?.name || '--'} @@ -199,14 +199,14 @@ function NodeTooltips({ if (!data) return null; let Component = null; const { model_type } = data; - if (model_type === NodeType.testDataset || model_type === NodeType.trainDataset) { + if (model_type === NodeType.TestDataset || model_type === NodeType.TrainDataset) { Component = ; - } else if (model_type === NodeType.project) { + } else if (model_type === NodeType.Project) { Component = ; } else if ( - model_type === NodeType.children || - model_type === NodeType.parent || - model_type === NodeType.current + model_type === NodeType.Children || + model_type === NodeType.Parent || + model_type === NodeType.Current ) { Component = ; } diff --git a/react-ui/src/pages/ModelDeployment/Create/index.tsx b/react-ui/src/pages/ModelDeployment/Create/index.tsx index 669130e6..0fa85fcf 100644 --- a/react-ui/src/pages/ModelDeployment/Create/index.tsx +++ b/react-ui/src/pages/ModelDeployment/Create/index.tsx @@ -5,22 +5,20 @@ */ import KFIcon from '@/components/KFIcon'; import PageTitle from '@/components/PageTitle'; -import ParameterInput from '@/components/ParameterInput'; +import ResourceSelect, { + requiredValidator, + type ParameterInputObject, +} from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; -import ResourceSelectorModal, { - ResourceSelectorResponse, - ResourceSelectorType, - selectorTypeConfig, -} from '@/pages/Pipeline/components/ResourceSelectorModal'; +import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; import { createModelDeploymentReq, restartModelDeploymentReq, updateModelDeploymentReq, } from '@/services/modelDeployment'; import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils'; -import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { getSessionStorageItem, @@ -39,13 +37,8 @@ import styles from './index.less'; export type FormData = { serviceName: string; // 服务名称 description: string; // 描述 - model: { - id: number; - version: string; - value: string; - showValue: string; - }; // 模型 - image: string; // 镜像 + model: ParameterInputObject; // 模型 + image: ParameterInputObject; // 镜像 resource: string; // 资源规格 replicas: string; // 副本数量 modelPath: string; // 模型路径 @@ -56,16 +49,10 @@ function ModelDeploymentCreate() { const navgite = useNavigate(); const [form] = Form.useForm(); const [resourceStandardList, filterResourceStandard] = useComputingResource(); - const [selectedModel, setSelectedModel] = useState( - undefined, - ); // 选择的模型,为了再次打开时恢复原来的选择 const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create); const [modelDeploymentInfo, setModelDeploymentInfo] = useState( undefined, ); - const [selectedMirror, setSelectedMirror] = useState( - undefined, - ); // 选择的镜像,为了再次打开时恢复原来的选择 const { message } = App.useApp(); useEffect(() => { @@ -81,65 +68,23 @@ function ModelDeploymentCreate() { }; }, []); - // 获取选择数据集、模型后面按钮 icon - const getSelectBtnIcon = (type: ResourceSelectorType) => { - return ; - }; - - // 选择模型、镜像 - const selectResource = (name: string, type: ResourceSelectorType) => { - let resource: ResourceSelectorResponse | undefined; - switch (type) { - case ResourceSelectorType.Model: - resource = selectedModel; - break; - default: - resource = selectedMirror; - break; - } - const { close } = openAntdModal(ResourceSelectorModal, { - type, - defaultExpandedKeys: resource ? [resource.id] : [], - defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], - defaultActiveTab: resource?.activeTab, - onOk: (res) => { - if (res) { - if (type === ResourceSelectorType.Mirror) { - form.setFieldValue(name, res.path); - setSelectedMirror(res); - } else { - const showValue = `${res.name}:${res.version}`; - form.setFieldValue(name, { - ...pick(res, ['id', 'version', 'path']), - showValue, - }); - setSelectedModel(res); - } - } else { - if (type === ResourceSelectorType.Model) { - setSelectedModel(undefined); - } else { - setSelectedMirror(undefined); - } - form.setFieldValue(name, ''); - } - close(); - }, - }); - }; - // 创建 const createModelDeployment = async (formData: FormData) => { const envList = formData['env'] ?? []; + const image = formData['image']; + const model = formData['model']; const env = envList.reduce((acc, cur) => { acc[cur.key] = cur.value; return acc; }, {} as Record); + // 根据后台要求,修改表单数据 const object = camelCaseToUnderscore({ - ...omit(formData, ['replicas', 'env']), + ...omit(formData, ['replicas', 'env', 'image', 'model']), replicas: Number(formData.replicas), env, + image: image.value, + model: pick(model, ['id', 'version', 'path', 'showValue']), }); const params = @@ -258,31 +203,21 @@ function ModelDeploymentCreate() { name="model" rules={[ { - required: true, + validator: requiredValidator, message: '请选择模型', }, ]} + required > - selectResource('model', ResourceSelectorType.Model)} /> - - - @@ -291,29 +226,20 @@ function ModelDeploymentCreate() { name="image" rules={[ { - required: true, - message: '请输入镜像', + validator: requiredValidator, + message: '请选择镜像', }, ]} + required > - selectResource('image', ResourceSelectorType.Mirror)} /> - - - diff --git a/react-ui/src/pages/ModelDeployment/List/index.tsx b/react-ui/src/pages/ModelDeployment/List/index.tsx index 934b4cbd..556f3f40 100644 --- a/react-ui/src/pages/ModelDeployment/List/index.tsx +++ b/react-ui/src/pages/ModelDeployment/List/index.tsx @@ -223,7 +223,7 @@ function ModelDeployment() { { title: '操作', dataIndex: 'operation', - width: 350, + width: 250, key: 'operation', render: (_: any, record: ModelDeploymentData) => (
@@ -336,6 +336,7 @@ function ModelDeployment() { total: total, showSizeChanger: true, showQuickJumper: true, + showTotal: () => `共${total}条`, }} onChange={handleTableChange} rowKey="service_id" diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx index 66562e13..01e30ab0 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx @@ -37,6 +37,7 @@ export type SelectorTypeInfo = { litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 tabItems: TabsProps['items']; // tab 列表 + buttontTitle: string; // 按钮 title }; // 获取镜像文件列表,为了兼容数据集和模型 @@ -77,6 +78,7 @@ export const selectorTypeConfig: Record label: '公开模型', }, ], + buttontTitle: '选择模型', }, [ResourceSelectorType.Dataset]: { getList: getDatasetList, @@ -98,6 +100,7 @@ export const selectorTypeConfig: Record label: '公开数据集', }, ], + buttontTitle: '选择数据集', }, [ResourceSelectorType.Mirror]: { getList: getMirrorListReq, @@ -121,5 +124,6 @@ export const selectorTypeConfig: Record label: '公开镜像', }, ], + buttontTitle: '选择镜像', }, }; diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx index 92b9e0b8..f4caf03a 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -39,7 +39,7 @@ export interface ResourceSelectorModalProps extends Omit { defaultExpandedKeys?: React.Key[]; defaultCheckedKeys?: React.Key[]; defaultActiveTab?: CommonTabKeys; - onOk?: (params: ResourceSelectorResponse | null) => void; + onOk?: (params: ResourceSelectorResponse | undefined) => void; } type TreeRef = GetRef>; @@ -279,7 +279,7 @@ function ResourceSelectorModal({ }; onOk?.(res); } else { - onOk?.(null); + onOk?.(undefined); } }; diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx index 2f434cb0..7d0f4f8e 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -86,30 +86,42 @@ const EditPipeline = () => { // 保存 const savePipeline = async (val) => { - const [res, error] = await to(paramsDrawerRef.current.validateFields()); - if (error) { + const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); + if (globalParamError) { message.error('全局参数配置有误'); openParamsDrawer(); return; } + closeParamsDrawer(); - const [propsRes, propsError] = await to(propsRef.current.getFieldsValue()); + const [propsRes, propsError] = await to(propsRef.current.validateFields()); if (propsError) { message.error('节点必填项必须配置'); return; } - propsRef.current.propClose(); + propsRef.current.close(); + setTimeout(() => { const data = graph.save(); console.log(data); + const errorNode = data.nodes.find((item) => { + return item.formError === true; + }); + if (errorNode) { + message.error(`【${errorNode.label}】节点必填项必须配置`); + const graphNode = graph.findById(errorNode.id); + if (graphNode) { + openNodeDrawer(graphNode, true); + } + return; + } const params = { ...locationParams, dag: JSON.stringify(data), - global_param: JSON.stringify(res.global_param), + global_param: JSON.stringify(globalParamRes.global_param), }; saveWorkflow(params).then((ret) => { message.success('保存成功'); - closeParamsDrawer(); setTimeout(() => { if (val) { navgite({ pathname: `/pipeline/template` }); @@ -122,10 +134,6 @@ const EditPipeline = () => { // 渲染数据 const getGraphData = (data) => { if (graph) { - // 修改历史数据有蓝色边框的问题 - data.nodes.forEach((item) => { - item.style.stroke = '#fff'; - }); graph.data(data); graph.render(); } else { @@ -283,6 +291,17 @@ const EditPipeline = () => { } }; + // 打开节点抽屉 + const openNodeDrawer = (node, validate = false) => { + // 获取所有的上游节点 + const parentNodes = findAllParentNodes(graph, node); + // 如果没有打开过全局参数抽屉,获取不到全局参数 + const globalParams = + paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; + // 打开节点编辑抽屉 + propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); + }; + // 初始化图 const initGraph = () => { const contextMenu = initMenu(); @@ -302,7 +321,7 @@ const EditPipeline = () => { ); }, afterDraw(cfg, group) { - const image = group.addShape('image', { + group.addShape('image', { attrs: { x: -45, y: -10, @@ -329,7 +348,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', { @@ -349,18 +387,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'); @@ -371,7 +401,7 @@ const EditPipeline = () => { point.show(); }); } else { - shape.attr('stroke', '#fff'); + shape.attr('stroke', 'transparent'); anchorPoints.forEach((point) => { point.hide(); }); @@ -467,9 +497,13 @@ const EditPipeline = () => { }, style: { fill: '#fff', - stroke: '#fff', + stroke: 'transparent', cursor: 'pointer', - radius: 10, + radius: 8, + shadowColor: 'rgba(75, 84, 137, 0.4)', + shadowBlur: 6, + shadowOffsetX: 0, + shadowOffsetY: 0, overflow: 'hidden', lineWidth: 0.5, }, @@ -500,19 +534,25 @@ const EditPipeline = () => { }, }); + // 修改历史数据样式问题 + graph.node((node) => { + return { + style: { + stroke: 'transparent', + radius: 8, + }, + }; + }); + + // 绑定事件 bindEvents(); }; + // 绑定事件 const bindEvents = () => { graph.on('node:click', (e) => { if (e.target.get('name') !== 'anchor-point' && e.item) { - // 获取所有的上游节点 - const parentNodes = findAllParentNodes(graph, e.item); - // 如果没有打开过全局参数抽屉,获取不到全局参数 - const globalParams = - paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; - // 打开节点编辑抽屉 - propsRef.current.showDrawer(e, globalParams, parentNodes); + openNodeDrawer(e.item); } }); graph.on('aftercreateedge', (e) => { @@ -599,36 +639,32 @@ const EditPipeline = () => { // 上下文菜单 const initMenu = () => { const contextMenu = new G6.Menu({ + className: 'pipeline-context-menu', getContent(evt) { const type = evt.item.getType(); - const cloneDisplay = type === 'node' ? 'block' : 'none'; + 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 @@ -685,7 +721,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 +38,27 @@ 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,45 +66,53 @@ 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()) { + showDrawer( + model: PipelineNodeModel, + params: PipelineGlobalParam[], + parentNodes: INode[], + validate: boolean = false, + ) { + try { + const nodeData: PipelineNodeModelSerialize = { + ...model, + in_parameters: JSON.parse(model.in_parameters), + out_parameters: JSON.parse(model.out_parameters), + control_strategy: JSON.parse(model.control_strategy), + }; + console.log('model', nodeData); + setStagingItem({ + ...nodeData, + }); form.resetFields(); - const model = e.item.getModel(); - try { - const nodeData = { - ...model, - in_parameters: JSON.parse(model.in_parameters), - out_parameters: JSON.parse(model.out_parameters), - control_strategy: JSON.parse(model.control_strategy), - }; - console.log('model', nodeData); - setStagingItem({ - ...nodeData, - }); - form.setFieldsValue({ - ...nodeData, - }); - } catch (error) { - console.log(error); + form.setFieldsValue({ + ...nodeData, + }); + if (validate) { + form.validateFields(); } - setOpen(true); - - // 参数下拉菜单 - setMenuItems(createMenuItems(params, parentNodes)); + } catch (error) { + console.log(error); } + setOpen(true); + + // 参数下拉菜单 + setMenuItems(createMenuItems(params, parentNodes)); }, - propClose: () => { + close: () => { onClose(); }, + validateFields: async () => { + if (!open) { + return; + } + const [values, error] = await to(form.validateFields()); + if (!error && values) { + return values; + } else { + form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); + return Promise.reject(error); + } + }, })); // 选择数据集、模型、镜像 @@ -115,7 +132,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 +178,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 +241,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 +273,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame
@@ -351,7 +393,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame value: 'standard', }} showSearch - allowClear />