| @@ -76,7 +76,7 @@ export default defineConfig({ | |||||
| * @name layout 插件 | * @name layout 插件 | ||||
| * @doc https://umijs.org/docs/max/layout-menu | * @doc https://umijs.org/docs/max/layout-menu | ||||
| */ | */ | ||||
| title: '智能软件开发平台', | |||||
| title: '智能材料科研平台', | |||||
| layout: { | layout: { | ||||
| locale: false, | locale: false, | ||||
| ...defaultSettings, | ...defaultSettings, | ||||
| @@ -16,7 +16,7 @@ const Settings: ProLayoutProps & { | |||||
| fixSiderbar: false, | fixSiderbar: false, | ||||
| splitMenus: false, | splitMenus: false, | ||||
| colorWeak: false, | colorWeak: false, | ||||
| title: '智能软件开发平台', | |||||
| title: '智能材料科研平台', | |||||
| pwa: true, | pwa: true, | ||||
| logo: '/assets/images/left-top-logo.png', | logo: '/assets/images/left-top-logo.png', | ||||
| token: { | token: { | ||||
| @@ -112,7 +112,7 @@ export default [ | |||||
| { | { | ||||
| name: '开发环境', | name: '开发环境', | ||||
| path: '', | path: '', | ||||
| component: './DevelopmentEnvironment/List', | |||||
| component: './DevelopmentEnvironment/Editor', | |||||
| }, | }, | ||||
| { | { | ||||
| name: '创建编辑器', | name: '创建编辑器', | ||||
| @@ -48,12 +48,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.1.0', | version: 'v0.1.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: null, | children_models: null, | ||||
| @@ -80,12 +101,38 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.3.0', | version: 'v0.3.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [], | children_models: [], | ||||
| @@ -110,12 +157,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.31.0', | version: 'v0.31.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [], | children_models: [], | ||||
| @@ -140,12 +208,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.4.0', | version: 'v0.4.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [ | children_models: [ | ||||
| @@ -154,12 +243,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.6.0', | version: 'v0.6.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [], | children_models: [], | ||||
| @@ -231,12 +341,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.5.0', | version: 'v0.5.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [ | children_models: [ | ||||
| @@ -275,12 +406,33 @@ export default defineMock({ | |||||
| exp_ins_id: null, | exp_ins_id: null, | ||||
| version: 'v0.11.0', | version: 'v0.11.0', | ||||
| ref_item: null, | 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_map: [], | ||||
| parent_models: [], | parent_models: [], | ||||
| children_models: [], | children_models: [], | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-17 12:53:06 | * @Date: 2024-04-17 12:53:06 | ||||
| * @Description: | |||||
| * @Description: 封装 iconfont 组件 | |||||
| */ | */ | ||||
| import '@/iconfont/iconfont-menu.js'; | import '@/iconfont/iconfont-menu.js'; | ||||
| import '@/iconfont/iconfont.js'; | import '@/iconfont/iconfont.js'; | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| import { Spin, SpinProps } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| function KFSpin(props: SpinProps) { | |||||
| return ( | |||||
| <div className={styles['kf-spin']}> | |||||
| <Spin {...props} /> | |||||
| <div className={styles['kf-spin__label']}>加载中</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default KFSpin; | |||||
| @@ -6,5 +6,5 @@ | |||||
| background-image: url(@/assets/img/page-title-bg.png); | background-image: url(@/assets/img/page-title-bg.png); | ||||
| background-repeat: no-repeat; | background-repeat: no-repeat; | ||||
| background-position: top center; | background-position: top center; | ||||
| background-size: 100%; | |||||
| background-size: 100% 100%; | |||||
| } | } | ||||
| @@ -62,3 +62,7 @@ | |||||
| font-size: 12px; | font-size: 12px; | ||||
| } | } | ||||
| } | } | ||||
| .parameter-input.parameter-input--error { | |||||
| border-color: @error-color; | |||||
| } | |||||
| @@ -1,18 +1,28 @@ | |||||
| import { CloseOutlined } from '@ant-design/icons'; | 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 classNames from 'classnames'; | ||||
| import './index.less'; | import './index.less'; | ||||
| type ParameterInputData = { | |||||
| value?: any; | |||||
| showValue?: any; | |||||
| fromSelect?: boolean; | |||||
| } & Record<string, any>; | |||||
| // 对象 | |||||
| export type ParameterInputObject = { | |||||
| value?: any; // 值 | |||||
| showValue?: any; // 显示值 | |||||
| fromSelect?: boolean; // 是否来自选择 | |||||
| activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab | |||||
| expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | |||||
| checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | |||||
| [key: string]: any; | |||||
| }; | |||||
| interface ParameterInputProps { | |||||
| value?: ParameterInputData; | |||||
| onChange?: (value: ParameterInputData) => void; | |||||
| // 值类型 | |||||
| export type ParameterInputValue = ParameterInputObject | string; | |||||
| export interface ParameterInputProps { | |||||
| value?: ParameterInputValue; | |||||
| onChange?: (value?: ParameterInputValue) => void; | |||||
| onClick?: () => void; | onClick?: () => void; | ||||
| onRemove?: () => void; | |||||
| canInput?: boolean; | canInput?: boolean; | ||||
| textArea?: boolean; | textArea?: boolean; | ||||
| placeholder?: string; | placeholder?: string; | ||||
| @@ -21,12 +31,14 @@ interface ParameterInputProps { | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| size?: 'middle' | 'small' | 'large'; | size?: 'middle' | 'small' | 'large'; | ||||
| disabled?: boolean; | disabled?: boolean; | ||||
| id?: string; | |||||
| } | } | ||||
| function ParameterInput({ | function ParameterInput({ | ||||
| value, | value, | ||||
| onChange, | onChange, | ||||
| onClick, | onClick, | ||||
| onRemove, | |||||
| canInput = true, | canInput = true, | ||||
| textArea = false, | textArea = false, | ||||
| allowClear, | allowClear, | ||||
| @@ -34,6 +46,7 @@ function ParameterInput({ | |||||
| style, | style, | ||||
| size = 'middle', | size = 'middle', | ||||
| disabled = false, | disabled = false, | ||||
| id, | |||||
| ...rest | ...rest | ||||
| }: ParameterInputProps) { | }: ParameterInputProps) { | ||||
| const valueObj = | const valueObj = | ||||
| @@ -42,16 +55,34 @@ function ParameterInput({ | |||||
| valueObj.showValue = valueObj.value; | valueObj.showValue = valueObj.value; | ||||
| } | } | ||||
| const isSelect = valueObj?.fromSelect; | const isSelect = valueObj?.fromSelect; | ||||
| const InputComponent = textArea ? Input.TextArea : Input; | |||||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | const placeholder = valueObj?.placeholder || rest?.placeholder; | ||||
| const InputComponent = textArea ? Input.TextArea : Input; | |||||
| const { status } = Form.Item.useStatus(); | |||||
| // 删除 | |||||
| const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { | |||||
| e.stopPropagation(); | |||||
| onChange?.({ | |||||
| ...valueObj, | |||||
| value: undefined, | |||||
| showValue: undefined, | |||||
| fromSelect: false, | |||||
| activeTab: undefined, | |||||
| expandedKeys: [], | |||||
| checkedKeys: [], | |||||
| }); | |||||
| onRemove?.(); | |||||
| }; | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {(isSelect || !canInput) && !disabled ? ( | {(isSelect || !canInput) && !disabled ? ( | ||||
| <div | <div | ||||
| id={id} | |||||
| className={classNames( | className={classNames( | ||||
| 'parameter-input', | 'parameter-input', | ||||
| { 'parameter-input--large': size === 'large' }, | { 'parameter-input--large': size === 'large' }, | ||||
| { [`parameter-input--${status}`]: status }, | |||||
| className, | className, | ||||
| )} | )} | ||||
| style={style} | style={style} | ||||
| @@ -62,18 +93,7 @@ function ParameterInput({ | |||||
| <span className="parameter-input__content__value">{valueObj?.showValue}</span> | <span className="parameter-input__content__value">{valueObj?.showValue}</span> | ||||
| <CloseOutlined | <CloseOutlined | ||||
| className="parameter-input__content__close-icon" | className="parameter-input__content__close-icon" | ||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| onChange?.({ | |||||
| ...valueObj, | |||||
| value: undefined, | |||||
| showValue: undefined, | |||||
| fromSelect: false, | |||||
| activeTab: undefined, | |||||
| expandedKeys: undefined, | |||||
| checkedKeys: undefined, | |||||
| }); | |||||
| }} | |||||
| onClick={handleRemove} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| ) : ( | ) : ( | ||||
| @@ -83,6 +103,7 @@ function ParameterInput({ | |||||
| ) : ( | ) : ( | ||||
| <InputComponent | <InputComponent | ||||
| {...rest} | {...rest} | ||||
| id={id} | |||||
| size={size} | size={size} | ||||
| className={className} | className={className} | ||||
| style={style} | style={style} | ||||
| @@ -93,9 +114,9 @@ function ParameterInput({ | |||||
| onChange={(e) => | onChange={(e) => | ||||
| onChange?.({ | onChange?.({ | ||||
| ...valueObj, | ...valueObj, | ||||
| fromSelect: false, | |||||
| value: e.target.value, | value: e.target.value, | ||||
| showValue: e.target.value, | showValue: e.target.value, | ||||
| fromSelect: false, | |||||
| }) | }) | ||||
| } | } | ||||
| /> | /> | ||||
| @@ -105,3 +126,12 @@ function ParameterInput({ | |||||
| } | } | ||||
| export default 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(); | |||||
| }; | |||||
| @@ -0,0 +1,11 @@ | |||||
| .kf-resource-select { | |||||
| position: relative; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| &__button { | |||||
| position: absolute; | |||||
| top: 0; | |||||
| left: calc(100% + 10px); | |||||
| } | |||||
| } | |||||
| @@ -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 <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | |||||
| }; | |||||
| function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps) { | |||||
| const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | |||||
| 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 ( | |||||
| <div className="kf-resource-select"> | |||||
| <ParameterInput | |||||
| {...rest} | |||||
| value={value} | |||||
| onChange={onChange} | |||||
| onRemove={() => setSelectedResource(undefined)} | |||||
| onClick={selectResource} | |||||
| ></ParameterInput> | |||||
| <Button | |||||
| className="kf-resource-select__button" | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(type)} | |||||
| onClick={selectResource} | |||||
| > | |||||
| {selectorTypeConfig[type].buttontTitle} | |||||
| </Button> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ResourceSelect; | |||||
| @@ -32,6 +32,7 @@ export function useStateRef<T>(initialValue: T) { | |||||
| */ | */ | ||||
| export function useVisible(initialValue: boolean) { | export function useVisible(initialValue: boolean) { | ||||
| const [visible, setVisible] = useState(initialValue); | const [visible, setVisible] = useState(initialValue); | ||||
| const ref = useRef(initialValue); | |||||
| const open = useCallback(() => { | const open = useCallback(() => { | ||||
| setVisible(true); | setVisible(true); | ||||
| @@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) { | |||||
| setVisible(false); | setVisible(false); | ||||
| }, []); | }, []); | ||||
| return [visible, open, close] as const; | |||||
| useEffect(() => { | |||||
| ref.current = visible; | |||||
| }, [visible]); | |||||
| return [visible, open, close, ref] as const; | |||||
| } | } | ||||
| type Callback<T> = (state: T) => void; | type Callback<T> = (state: T) => void; | ||||
| @@ -1,35 +1,32 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 创建镜像 | |||||
| * @Description: 创建开发环境 | |||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; | import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import ParameterInput from '@/components/ParameterInput'; | |||||
| import ResourceSelect, { | |||||
| requiredValidator, | |||||
| type ParameterInputObject, | |||||
| } from '@/components/ResourceSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | 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 { createEditorReq } from '@/services/developmentEnvironment'; | ||||
| import { openAntdModal } from '@/utils/modal'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { App, Button, Col, Form, Input, Row, Select } from 'antd'; | 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'; | import styles from './index.less'; | ||||
| type FormData = { | type FormData = { | ||||
| name: string; | name: string; | ||||
| computing_resource: string; | computing_resource: string; | ||||
| standard: string; | standard: string; | ||||
| image: string; | |||||
| model: ResourceSelectorResponse; | |||||
| dataset: ResourceSelectorResponse; | |||||
| image: ParameterInputObject; | |||||
| model: ParameterInputObject; | |||||
| dataset: ParameterInputObject; | |||||
| }; | }; | ||||
| enum ComputingResourceType { | enum ComputingResourceType { | ||||
| @@ -55,25 +52,20 @@ function EditorCreate() { | |||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | const [resourceStandardList, filterResourceStandard] = useComputingResource(); | ||||
| const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>( | |||||
| undefined, | |||||
| ); // 选择的模型,为了再次打开时恢复原来的选择 | |||||
| const [selectedDataset, setSelectedDataset] = useState<ResourceSelectorResponse | undefined>( | |||||
| undefined, | |||||
| ); // 选择的数据集,为了再次打开时恢复原来的选择 | |||||
| const [selectedMirror, setSelectedMirror] = useState<ResourceSelectorResponse | undefined>( | |||||
| undefined, | |||||
| ); // 选择的镜像,为了再次打开时恢复原来的选择 | |||||
| // 创建编辑器 | // 创建编辑器 | ||||
| const createEditor = async (formData: FormData) => { | 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) { | if (res) { | ||||
| message.success('创建成功'); | message.success('创建成功'); | ||||
| navgite(-1); | navgite(-1); | ||||
| @@ -89,61 +81,6 @@ function EditorCreate() { | |||||
| const cancel = () => { | const cancel = () => { | ||||
| navgite(-1); | navgite(-1); | ||||
| }; | }; | ||||
| // 获取选择数据集、模型后面按钮 icon | |||||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | |||||
| return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | |||||
| }; | |||||
| // 选择模型、镜像、数据集 | |||||
| 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 ( | return ( | ||||
| <div className={styles['editor-create']}> | <div className={styles['editor-create']}> | ||||
| @@ -230,64 +167,46 @@ function EditorCreate() { | |||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="镜像" | |||||
| label="镜 像" | |||||
| name="image" | name="image" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | |||||
| message: '请输入镜像', | |||||
| validator: requiredValidator, | |||||
| message: '请选择镜像', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| required | |||||
| > | > | ||||
| <ParameterInput | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Mirror} | |||||
| placeholder="请选择镜像" | placeholder="请选择镜像" | ||||
| canInput={false} | canInput={false} | ||||
| size="large" | size="large" | ||||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| <Col span={10}> | |||||
| <Button | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(ResourceSelectorType.Mirror)} | |||||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||||
| > | |||||
| 选择镜像 | |||||
| </Button> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="模型" | |||||
| label="模 型" | |||||
| name="model" | name="model" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | |||||
| validator: requiredValidator, | |||||
| message: '请选择模型', | message: '请选择模型', | ||||
| }, | }, | ||||
| ]} | ]} | ||||
| required | |||||
| > | > | ||||
| <ParameterInput | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Model} | |||||
| placeholder="请选择模型" | placeholder="请选择模型" | ||||
| canInput={false} | canInput={false} | ||||
| size="large" | size="large" | ||||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| <Col span={10}> | |||||
| <Button | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(ResourceSelectorType.Model)} | |||||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||||
| > | |||||
| 选择模型 | |||||
| </Button> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| @@ -296,29 +215,20 @@ function EditorCreate() { | |||||
| name="dataset" | name="dataset" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | |||||
| validator: requiredValidator, | |||||
| message: '请选择数据集', | message: '请选择数据集', | ||||
| }, | }, | ||||
| ]} | ]} | ||||
| required | |||||
| > | > | ||||
| <ParameterInput | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Dataset} | |||||
| placeholder="请选择数据集" | placeholder="请选择数据集" | ||||
| canInput={false} | canInput={false} | ||||
| size="large" | size="large" | ||||
| onClick={() => selectResource('dataset', ResourceSelectorType.Dataset)} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| <Col span={10}> | |||||
| <Button | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(ResourceSelectorType.Dataset)} | |||||
| onClick={() => selectResource('dataset', ResourceSelectorType.Dataset)} | |||||
| > | |||||
| 选择数据集 | |||||
| </Button> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 开发环境 | |||||
| * @Description: 开发环境列表 | |||||
| */ | */ | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | import CommonTableCell from '@/components/CommonTableCell'; | ||||
| import DateTableCell from '@/components/DateTableCell'; | import DateTableCell from '@/components/DateTableCell'; | ||||
| @@ -0,0 +1,17 @@ | |||||
| export enum ComparisonType { | |||||
| Train = 'Train', // 训练 | |||||
| Evaluate = 'Evaluate', // 评估 | |||||
| } | |||||
| type ComparisonTypeInfo = { | |||||
| title: string; | |||||
| }; | |||||
| export const comparisonConfig: Record<ComparisonType, ComparisonTypeInfo> = { | |||||
| [ComparisonType.Train]: { | |||||
| title: '训练', | |||||
| }, | |||||
| [ComparisonType.Evaluate]: { | |||||
| title: '评估', | |||||
| }, | |||||
| }; | |||||
| @@ -22,6 +22,12 @@ | |||||
| .ant-table-container { | .ant-table-container { | ||||
| border: none !important; | border: none !important; | ||||
| } | } | ||||
| .ant-table-thead { | |||||
| .ant-table-cell { | |||||
| background-color: rgb(247, 247, 247); | |||||
| border-color: #e8e8e8 !important; | |||||
| } | |||||
| } | |||||
| .ant-table-tbody { | .ant-table-tbody { | ||||
| .ant-table-cell { | .ant-table-cell { | ||||
| border-right: none !important; | border-right: none !important; | ||||
| @@ -7,17 +7,13 @@ import { | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | ||||
| import { useSearchParams } from '@umijs/max'; | 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 classNames from 'classnames'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | import ExperimentStatusCell from '../components/ExperimentStatusCell'; | ||||
| import { ComparisonType, comparisonConfig } from './config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export enum ComparisonType { | |||||
| Train = 'Train', // 训练 | |||||
| Evaluate = 'Evaluate', // 评估 | |||||
| } | |||||
| type TableData = { | type TableData = { | ||||
| experiment_ins_id: number; | experiment_ins_id: number; | ||||
| run_id: string; | run_id: string; | ||||
| @@ -32,13 +28,15 @@ type TableData = { | |||||
| function ExperimentComparison() { | function ExperimentComparison() { | ||||
| const [searchParams] = useSearchParams(); | const [searchParams] = useSearchParams(); | ||||
| const comparisonType = searchParams.get('type'); | |||||
| const comparisonType = searchParams.get('type') as ComparisonType; | |||||
| const experimentId = searchParams.get('id'); | const experimentId = searchParams.get('id'); | ||||
| const [tableData, setTableData] = useState<TableData[]>([]); | const [tableData, setTableData] = useState<TableData[]>([]); | ||||
| // const [cacheState, setCacheState] = useCacheState(); | // const [cacheState, setCacheState] = useCacheState(); | ||||
| // const [total, setTotal] = useState(0); | // const [total, setTotal] = useState(0); | ||||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | ||||
| const [loading, setLoading] = useState(false); | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); | |||||
| // const [pagination, setPagination] = useState<TablePaginationConfig>( | // const [pagination, setPagination] = useState<TablePaginationConfig>( | ||||
| // cacheState?.pagination ?? { | // cacheState?.pagination ?? { | ||||
| // current: 1, | // current: 1, | ||||
| @@ -52,9 +50,11 @@ function ExperimentComparison() { | |||||
| // 获取对比数据列表 | // 获取对比数据列表 | ||||
| const getComparisonData = async () => { | const getComparisonData = async () => { | ||||
| setLoading(true); | |||||
| const request = | const request = | ||||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | ||||
| const [res] = await to(request(experimentId)); | const [res] = await to(request(experimentId)); | ||||
| setLoading(false); | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| // const { content = [], totalElements = 0 } = res.data; | // const { content = [], totalElements = 0 } = res.data; | ||||
| setTableData(res.data); | setTableData(res.data); | ||||
| @@ -91,6 +91,7 @@ function ExperimentComparison() { | |||||
| // 选择行 | // 选择行 | ||||
| const rowSelection: TableProps['rowSelection'] = { | const rowSelection: TableProps['rowSelection'] = { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| fixed: 'left', | |||||
| selectedRowKeys, | selectedRowKeys, | ||||
| onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { | onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { | ||||
| console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); | console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); | ||||
| @@ -108,7 +109,9 @@ function ExperimentComparison() { | |||||
| title: '实例 ID', | title: '实例 ID', | ||||
| dataIndex: 'experiment_ins_id', | dataIndex: 'experiment_ins_id', | ||||
| key: 'experiment_ins_id', | key: 'experiment_ins_id', | ||||
| width: '20%', | |||||
| width: 100, | |||||
| fixed: 'left', | |||||
| align: 'center', | |||||
| render: tableCellRender(), | render: tableCellRender(), | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -116,43 +119,61 @@ function ExperimentComparison() { | |||||
| dataIndex: 'start_time', | dataIndex: 'start_time', | ||||
| key: 'start_time', | key: 'start_time', | ||||
| width: 180, | width: 180, | ||||
| fixed: 'left', | |||||
| align: 'center', | |||||
| render: tableCellRender(false, dateFormatter), | render: tableCellRender(false, dateFormatter), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '运行状态', | title: '运行状态', | ||||
| dataIndex: 'status', | dataIndex: 'status', | ||||
| key: 'status', | key: 'status', | ||||
| width: '20%', | |||||
| width: 100, | |||||
| fixed: 'left', | |||||
| align: 'center', | |||||
| render: ExperimentStatusCell, | render: ExperimentStatusCell, | ||||
| }, | }, | ||||
| { | { | ||||
| title: '训练数据集', | |||||
| title: `${config.title}数据集`, | |||||
| dataIndex: 'dataset', | dataIndex: 'dataset', | ||||
| key: 'dataset', | key: 'dataset', | ||||
| width: '20%', | |||||
| width: 180, | |||||
| fixed: 'left', | |||||
| align: 'center', | |||||
| render: tableCellRender(true, arrayFormatter()), | render: tableCellRender(true, arrayFormatter()), | ||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| title: '训练参数', | |||||
| title: `${config.title}参数`, | |||||
| align: 'center', | |||||
| children: first?.params_names.map((name) => ({ | children: first?.params_names.map((name) => ({ | ||||
| title: name, | |||||
| title: ( | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| dataIndex: ['params', name], | dataIndex: ['params', name], | ||||
| key: name, | key: name, | ||||
| width: '20%', | |||||
| width: 120, | |||||
| align: 'center', | |||||
| render: tableCellRender(true), | render: tableCellRender(true), | ||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| })), | })), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '训练指标', | |||||
| title: `${config.title}指标`, | |||||
| align: 'center', | |||||
| children: first?.metrics_names.map((name) => ({ | children: first?.metrics_names.map((name) => ({ | ||||
| title: name, | |||||
| title: ( | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| dataIndex: ['metrics', name], | dataIndex: ['metrics', name], | ||||
| key: name, | key: name, | ||||
| width: '20%', | |||||
| width: 120, | |||||
| align: 'center', | |||||
| render: tableCellRender(true), | render: tableCellRender(true), | ||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| })), | })), | ||||
| @@ -177,9 +198,10 @@ function ExperimentComparison() { | |||||
| dataSource={tableData} | dataSource={tableData} | ||||
| columns={columns} | columns={columns} | ||||
| rowSelection={rowSelection} | rowSelection={rowSelection} | ||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||||
| pagination={false} | pagination={false} | ||||
| bordered={true} | bordered={true} | ||||
| loading={loading} | |||||
| // pagination={{ | // pagination={{ | ||||
| // ...pagination, | // ...pagination, | ||||
| // total: total, | // total: total, | ||||
| @@ -1,76 +1,158 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useStateRef, useVisible } from '@/hooks'; | import { useStateRef, useVisible } from '@/hooks'; | ||||
| import { getExperimentIns } from '@/services/experiment/index.js'; | import { getExperimentIns } from '@/services/experiment/index.js'; | ||||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | import { getWorkflowById } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { fittingString } from '@/utils'; | import { fittingString } from '@/utils'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | 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 { Button } from 'antd'; | ||||
| import { useEffect, useRef } from 'react'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import { useNavigate, useParams } from 'react-router-dom'; | import { useNavigate, useParams } from 'react-router-dom'; | ||||
| import ParamsModal from '../components/ViewParamsModal'; | import ParamsModal from '../components/ViewParamsModal'; | ||||
| import { experimentStatusInfo } from '../status'; | import { experimentStatusInfo } from '../status'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import Props from './props'; | |||||
| import ExperimentDrawer from './props'; | |||||
| let graph = null; | let graph = null; | ||||
| function ExperimentText() { | 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 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.data(data); | ||||
| graph.render(); | 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 { | } else { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| getGraphData(data); | getGraphData(data); | ||||
| }, 500); | }, 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 = () => { | const initGraph = () => { | ||||
| G6.registerNode( | G6.registerNode( | ||||
| @@ -116,6 +198,54 @@ function ExperimentText() { | |||||
| draggable: true, | 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 bbox = group.getBBox(); | ||||
| const anchorPoints = this.getAnchorPoints(cfg); | const anchorPoints = this.getAnchorPoints(cfg); | ||||
| anchorPoints.forEach((anchorPos, i) => { | anchorPoints.forEach((anchorPos, i) => { | ||||
| @@ -139,12 +269,12 @@ function ExperimentText() { | |||||
| // response the state changes and show/hide the link-point circles | // response the state changes and show/hide the link-point circles | ||||
| setState(name, value, item) { | setState(name, value, item) { | ||||
| const group = item.getContainer(); | const group = item.getContainer(); | ||||
| const shape = group.get('children')[0]; | |||||
| const shape = group.get('children')?.[0]; | |||||
| if (name === 'hover') { | if (name === 'hover') { | ||||
| if (value) { | if (value) { | ||||
| shape.attr('stroke', themes['primaryColor']); | |||||
| shape?.attr('stroke', themes['primaryColor']); | |||||
| } else { | } else { | ||||
| shape.attr('stroke', '#fff'); | |||||
| shape?.attr('stroke', 'transparent'); | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -181,7 +311,7 @@ function ExperimentText() { | |||||
| defaultNode: { | defaultNode: { | ||||
| type: 'rect-node', | type: 'rect-node', | ||||
| size: [110, 36], | |||||
| size: [width, height], | |||||
| labelCfg: { | labelCfg: { | ||||
| style: { | style: { | ||||
| @@ -196,8 +326,14 @@ function ExperimentText() { | |||||
| }, | }, | ||||
| style: { | style: { | ||||
| fill: '#fff', | 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, | 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) => { | graph.on('node:click', (e) => { | ||||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | 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) => { | graph.on('node:mouseenter', (e) => { | ||||
| @@ -235,22 +390,17 @@ function ExperimentText() { | |||||
| graph.on('node:mouseleave', (e) => { | graph.on('node:mouseleave', (e) => { | ||||
| graph.setItemState(e.item, 'hover', false); | 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 ( | return ( | ||||
| <div className={styles['pipeline-container']}> | <div className={styles['pipeline-container']}> | ||||
| <div className={styles['pipeline-container__top']}> | <div className={styles['pipeline-container__top']}> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 启动时间:{formatDate(message.create_time)} | |||||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 执行时长: | 执行时长: | ||||
| {elapsedTime(message.create_time, message.finish_time)} | |||||
| {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 状态: | 状态: | ||||
| @@ -260,11 +410,11 @@ function ExperimentText() { | |||||
| height: '8px', | height: '8px', | ||||
| borderRadius: '50%', | borderRadius: '50%', | ||||
| marginRight: '6px', | marginRight: '6px', | ||||
| backgroundColor: experimentStatusInfo[message.status]?.color, | |||||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||||
| }} | }} | ||||
| ></div> | ></div> | ||||
| <span style={{ color: experimentStatusInfo[message.status]?.color }}> | |||||
| {experimentStatusInfo[message.status]?.label} | |||||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||||
| </span> | </span> | ||||
| </div> | </div> | ||||
| <Button | <Button | ||||
| @@ -275,11 +425,24 @@ function ExperimentText() { | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | ||||
| <Props ref={propsRef}></Props> | |||||
| {experimentNodeData ? ( | |||||
| <ExperimentDrawer | |||||
| open={propsDrawerOpen} | |||||
| onClose={closePropsDrawer} | |||||
| instanceId={experimentIns?.id} | |||||
| instanceName={experimentIns?.argo_ins_name} | |||||
| instanceNamespace={experimentIns?.argo_ins_ns} | |||||
| instanceNodeData={experimentNodeData} | |||||
| workflowId={experimentNodeData.workflowId} | |||||
| instanceNodeStatus={experimentNodeData.experimentStatus} | |||||
| instanceNodeStartTime={experimentNodeData.experimentStartTime} | |||||
| instanceNodeEndTime={experimentIns.experimentEndTime} | |||||
| ></ExperimentDrawer> | |||||
| ) : null} | |||||
| <ParamsModal | <ParamsModal | ||||
| open={paramsModalOpen} | open={paramsModalOpen} | ||||
| onCancel={closeParamsModal} | onCancel={closeParamsModal} | ||||
| globalParam={message.global_param} | |||||
| globalParam={experimentIns?.global_param} | |||||
| ></ParamsModal> | ></ParamsModal> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -30,4 +30,10 @@ | |||||
| background-image: url(/assets/images/pipeline-canvas-back.png); | background-image: url(/assets/images/pipeline-canvas-back.png); | ||||
| background-size: 100% 100%; | background-size: 100% 100%; | ||||
| } | } | ||||
| :global { | |||||
| .ant-drawer-mask { | |||||
| background: transparent !important; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -14,7 +14,12 @@ | |||||
| border: 1px solid #e0eaff; | border: 1px solid #e0eaff; | ||||
| } | } | ||||
| .ant-tabs-content-holder { | .ant-tabs-content-holder { | ||||
| overflow-y: auto; | |||||
| .ant-tabs-content { | |||||
| height: 100%; | |||||
| .ant-tabs-tabpane { | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,11 +1,9 @@ | |||||
| import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | import { elapsedTime, formatDate } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | |||||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | 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 ExperimentParameter from '../components/ExperimentParameter'; | ||||
| import ExperimentResult from '../components/ExperimentResult'; | import ExperimentResult from '../components/ExperimentResult'; | ||||
| import LogList from '../components/LogList'; | import LogList from '../components/LogList'; | ||||
| @@ -19,154 +17,130 @@ export type ExperimentLog = { | |||||
| start_time?: string; // 日志开始时间 | start_time?: string; // 日志开始时间 | ||||
| }; | }; | ||||
| const Props = forwardRef((_, ref) => { | |||||
| const [form] = Form.useForm(); | |||||
| const [experimentNodeData, setExperimentNodeData] = useState<PipelineNodeModelSerialize>( | |||||
| {} as PipelineNodeModelSerialize, | |||||
| ); | |||||
| const [experimentResults, setExperimentResults] = useState([]); | |||||
| const [experimentLogList, setExperimentLogList] = useState<ExperimentLog[]>([]); | |||||
| 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: ( | |||||
| <LogList list={experimentLogList} status={experimentNodeData.experimentStatus}></LogList> | |||||
| ), | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| { | |||||
| key: '2', | |||||
| label: '配置参数', | |||||
| icon: <DatabaseOutlined />, | |||||
| children: <ExperimentParameter form={form} nodeData={experimentNodeData} />, | |||||
| }, | |||||
| const ExperimentDrawer = forwardRef( | |||||
| ( | |||||
| { | { | ||||
| key: '3', | |||||
| label: '输出结果', | |||||
| children: <ExperimentResult results={experimentResults}></ExperimentResult>, | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| ]; | |||||
| 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: ( | |||||
| <LogList | |||||
| instanceName={instanceName} | |||||
| instanceNamespace={instanceNamespace} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| workflowId={workflowId} | |||||
| instanceNodeStartTime={instanceNodeStartTime} | |||||
| instanceNodeStatus={instanceNodeStatus} | |||||
| ></LogList> | |||||
| ), | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| { | |||||
| key: '2', | |||||
| label: '配置参数', | |||||
| icon: <DatabaseOutlined />, | |||||
| children: <ExperimentParameter nodeData={instanceNodeData} />, | |||||
| }, | |||||
| { | |||||
| key: '3', | |||||
| label: '输出结果', | |||||
| children: ( | |||||
| <ExperimentResult | |||||
| experimentInsId={instanceId} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| ></ExperimentResult> | |||||
| ), | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| ], | |||||
| [ | |||||
| 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 ( | |||||
| <Drawer | |||||
| title="任务执行详情" | |||||
| placement="right" | |||||
| getContainer={false} | |||||
| closeIcon={false} | |||||
| onClose={onClose} | |||||
| open={open} | |||||
| width={520} | |||||
| className={styles['experiment-drawer']} | |||||
| destroyOnClose={true} | |||||
| > | |||||
| <div style={{ paddingTop: '15px' }}> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 任务名称:{experimentNodeData.label} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 执行状态: | |||||
| <div | |||||
| className={styles['experiment-drawer__status-dot']} | |||||
| style={{ | |||||
| backgroundColor: experimentStatusInfo[experimentNodeData.experimentStatus]?.color, | |||||
| }} | |||||
| ></div> | |||||
| <span style={{ color: experimentStatusInfo[experimentNodeData.experimentStatus]?.color }}> | |||||
| {experimentStatusInfo[experimentNodeData.experimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 启动时间:{formatDate(experimentNodeData.experimentStartTime)} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 耗时: | |||||
| {elapsedTime( | |||||
| experimentNodeData.experimentStartTime, | |||||
| experimentNodeData.experimentEndTime, | |||||
| )} | |||||
| return ( | |||||
| <Drawer | |||||
| title="任务执行详情" | |||||
| placement="right" | |||||
| getContainer={false} | |||||
| closeIcon={false} | |||||
| onClose={onClose} | |||||
| open={open} | |||||
| width={520} | |||||
| className={styles['experiment-drawer']} | |||||
| destroyOnClose={true} | |||||
| > | |||||
| <div style={{ paddingTop: '15px' }}> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 任务名称:{instanceNodeData.label} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 执行状态: | |||||
| {instanceNodeStatus ? ( | |||||
| <> | |||||
| <div | |||||
| className={styles['experiment-drawer__status-dot']} | |||||
| style={{ | |||||
| backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color, | |||||
| }} | |||||
| ></div> | |||||
| <span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}> | |||||
| {experimentStatusInfo[instanceNodeStatus]?.label} | |||||
| </span> | |||||
| </> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 启动时间:{formatDate(instanceNodeStartTime)} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 耗时: | |||||
| {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||||
| </Drawer> | |||||
| ); | |||||
| }); | |||||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||||
| </Drawer> | |||||
| ); | |||||
| }, | |||||
| ); | |||||
| export default Props; | |||||
| export default ExperimentDrawer; | |||||
| @@ -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; | |||||
| } | |||||
| @@ -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 ( | |||||
| <div> | |||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||||
| <div className={styles.index}>序号</div> | |||||
| <div className={styles.tensorBoard}>可视化</div> | |||||
| <div className={styles.description}> | |||||
| <div style={{ width: '50%' }}>运行时长</div> | |||||
| <div style={{ width: '50%' }}>开始时间</div> | |||||
| </div> | |||||
| <div className={styles.status}>状态</div> | |||||
| <div className={styles.operation}>操作</div> | |||||
| </div> | |||||
| {experimentInList.map((item, index) => ( | |||||
| <div | |||||
| key={item.id} | |||||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | |||||
| > | |||||
| <a | |||||
| className={styles.index} | |||||
| style={{ padding: '0 16px' }} | |||||
| onClick={() => onClickInstance?.(item)} | |||||
| > | |||||
| {index + 1} | |||||
| </a> | |||||
| <div className={styles.tensorBoard}> | |||||
| {item.nodes_result?.tensorboard_log ? ( | |||||
| <TensorBoardStatusCell | |||||
| status={item.tensorBoardStatus} | |||||
| onClick={() => onClickTensorBoard?.(item)} | |||||
| ></TensorBoardStatusCell> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | |||||
| <div className={styles.description}> | |||||
| <div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div> | |||||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||||
| <Tooltip title={formatDate(item.create_time)}> | |||||
| <span>{formatDate(item.create_time)}</span> | |||||
| </Tooltip> | |||||
| </div> | |||||
| </div> | |||||
| <div className={styles.statusBox}> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||||
| /> | |||||
| <span | |||||
| style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} | |||||
| className={styles.statusIcon} | |||||
| > | |||||
| {experimentStatusInfo[item.status as ExperimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <div className={styles.operation}> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="stop" | |||||
| disabled={ | |||||
| item.status === ExperimentStatus.Succeeded || | |||||
| item.status === ExperimentStatus.Failed || | |||||
| item.status === ExperimentStatus.Terminated | |||||
| } | |||||
| icon={<KFIcon type="icon-zhongzhi" />} | |||||
| onClick={() => terminateExperimentInstance(item)} | |||||
| > | |||||
| 终止 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="batchRemove" | |||||
| disabled={ | |||||
| item.status === ExperimentStatus.Running || | |||||
| item.status === ExperimentStatus.Pending | |||||
| } | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleRemove(item)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| </div> | |||||
| ))} | |||||
| {experimentInsTotal > experimentInList.length ? ( | |||||
| <div className={styles.loadMoreBox}> | |||||
| <Button type="link" onClick={onLoadMore}> | |||||
| 更多 | |||||
| <DoubleRightOutlined rotate={90} /> | |||||
| </Button> | |||||
| </div> | |||||
| ) : null} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentInstanceComponent; | |||||
| @@ -1,5 +1,7 @@ | |||||
| .experiment-parameter { | .experiment-parameter { | ||||
| height: 100%; | |||||
| padding-top: 8px; | padding-top: 8px; | ||||
| overflow-y: auto; | |||||
| &__title { | &__title { | ||||
| display: flex; | display: flex; | ||||
| @@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { Form, Input, Select, type FormProps } from 'antd'; | |||||
| import { Form, Input, Select } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { TextArea } = Input; | const { TextArea } = Input; | ||||
| type ExperimentParameterProps = { | type ExperimentParameterProps = { | ||||
| form: FormProps['form']; | |||||
| nodeData: PipelineNodeModelSerialize; | nodeData: PipelineNodeModelSerialize; | ||||
| }; | }; | ||||
| function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| const [resourceStandardList] = useComputingResource(); // 资源规模 | const [resourceStandardList] = useComputingResource(); // 资源规模 | ||||
| // 控制策略 | // 控制策略 | ||||
| @@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||||
| wrapperCol={{ | wrapperCol={{ | ||||
| span: 24, | span: 24, | ||||
| }} | }} | ||||
| form={form} | |||||
| initialValues={nodeData} | |||||
| style={{ | style={{ | ||||
| maxWidth: 600, | maxWidth: 600, | ||||
| }} | }} | ||||
| @@ -1,5 +1,7 @@ | |||||
| .experiment-result { | .experiment-result { | ||||
| height: 100%; | |||||
| padding: 8px; | padding: 8px; | ||||
| overflow-y: auto; | |||||
| color: @text-color; | color: @text-color; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| @@ -1,11 +1,15 @@ | |||||
| import { getNodeResult } from '@/services/experiment/index.js'; | |||||
| import { downLoadZip } from '@/utils/downloadfile'; | import { downLoadZip } from '@/utils/downloadfile'; | ||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | |||||
| import { App, Button } from 'antd'; | import { App, Button } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import ExportModelModal from '../ExportModelModal'; | import ExportModelModal from '../ExportModelModal'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| results?: ExperimentResultData[] | null; | |||||
| experimentInsId?: number; // 实验实例 id | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| }; | }; | ||||
| type ExperimentResultData = { | type ExperimentResultData = { | ||||
| @@ -18,8 +22,21 @@ type ExperimentResultData = { | |||||
| }[]; | }[]; | ||||
| }; | }; | ||||
| function ExperimentResult({ results }: ExperimentResultProps) { | |||||
| function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const [experimentResults, setExperimentResults] = useState<ExperimentResultData[]>([]); | |||||
| 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) => { | const download = (path: string) => { | ||||
| @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-result']}> | <div className={styles['experiment-result']}> | ||||
| <div className={styles['experiment-result__content']}> | <div className={styles['experiment-result__content']}> | ||||
| {results && results.length > 0 ? ( | |||||
| results.map((item) => ( | |||||
| <div key={item.name} className={styles['experiment-result__item']}> | |||||
| {experimentResults.length > 0 ? ( | |||||
| experimentResults.map((item) => ( | |||||
| <div key={item.name || item.path} className={styles['experiment-result__item']}> | |||||
| <div className={styles['experiment-result__item__name']}> | <div className={styles['experiment-result__item__name']}> | ||||
| <span>{item.name}</span> | <span>{item.name}</span> | ||||
| <Button | <Button | ||||
| @@ -11,11 +11,11 @@ import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export type LogGroupProps = ExperimentLog & { | export type LogGroupProps = ExperimentLog & { | ||||
| status: ExperimentStatus; // 实验状态 | |||||
| status?: ExperimentStatus; // 实验状态 | |||||
| }; | }; | ||||
| type Log = { | type Log = { | ||||
| @@ -25,7 +25,7 @@ type Log = { | |||||
| // 滚动到底部 | // 滚动到底部 | ||||
| const scrollToBottom = (smooth: boolean = true) => { | const scrollToBottom = (smooth: boolean = true) => { | ||||
| const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; | |||||
| const element = document.getElementById('log-list'); | |||||
| if (element) { | if (element) { | ||||
| const optons: ScrollToOptions = { | const optons: ScrollToOptions = { | ||||
| top: element.scrollHeight, | top: element.scrollHeight, | ||||
| @@ -41,25 +41,36 @@ function LogGroup({ | |||||
| pod_name = '', | pod_name = '', | ||||
| log_content = '', | log_content = '', | ||||
| start_time, | start_time, | ||||
| status = ExperimentStatus.Pending, | |||||
| status, | |||||
| }: LogGroupProps) { | }: LogGroupProps) { | ||||
| const [collapse, setCollapse] = useState(true); | const [collapse, setCollapse] = useState(true); | ||||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | ||||
| const [completed, setCompleted] = useState(false); | const [completed, setCompleted] = useState(false); | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| let timerId: NodeJS.Timeout | undefined; | |||||
| if (status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Running) { | ||||
| const timerId = setInterval(() => { | |||||
| timerId = setInterval(() => { | |||||
| requestExperimentPodsLog(); | requestExperimentPodsLog(); | ||||
| }, 5000); | |||||
| return () => { | |||||
| clearInterval(timerId); | |||||
| }; | |||||
| }, 5 * 1000); | |||||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | |||||
| requestExperimentPodsLog(); | |||||
| setTimeout(() => { | |||||
| requestExperimentPodsLog(); | |||||
| }, 5 * 1000); | |||||
| } | } | ||||
| }, []); | |||||
| preStatusRef.current = status; | |||||
| return () => { | |||||
| if (timerId) { | |||||
| clearInterval(timerId); | |||||
| timerId = undefined; | |||||
| } | |||||
| }; | |||||
| }, [status]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const mouseDown = () => { | const mouseDown = () => { | ||||
| @@ -131,7 +142,8 @@ function LogGroup({ | |||||
| const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | ||||
| const logText = log_content + logList.map((v) => v.log_content).join(''); | const logText = log_content + logList.map((v) => v.log_content).join(''); | ||||
| const showMoreBtn = status !== 'Running' && showLog && !completed && logText !== ''; | |||||
| const showMoreBtn = | |||||
| status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; | |||||
| return ( | return ( | ||||
| <div className={styles['log-group']}> | <div className={styles['log-group']}> | ||||
| {log_type === 'resource' && ( | {log_type === 'resource' && ( | ||||
| @@ -1,5 +1,7 @@ | |||||
| .log-list { | .log-list { | ||||
| height: 100%; | |||||
| padding: 8px; | padding: 8px; | ||||
| overflow-y: auto; | |||||
| &__empty { | &__empty { | ||||
| padding: 15px; | padding: 15px; | ||||
| @@ -1,18 +1,74 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | import { ExperimentLog } from '@/pages/Experiment/Info/props'; | ||||
| import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import dayjs from 'dayjs'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import LogGroup from '../LogGroup'; | import LogGroup from '../LogGroup'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type LogListProps = { | type LogListProps = { | ||||
| list: ExperimentLog[]; | |||||
| status: ExperimentStatus; | |||||
| instanceName?: string; // 实验实例 name | |||||
| instanceNamespace?: string; // 实验实例 namespace | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| workflowId?: string; // 实验实例工作流 id | |||||
| instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | |||||
| instanceNodeStatus?: ExperimentStatus; | |||||
| }; | }; | ||||
| function LogList({ list = [], status }: LogListProps) { | |||||
| function LogList({ | |||||
| instanceName, | |||||
| instanceNamespace, | |||||
| pipelineNodeId, | |||||
| workflowId, | |||||
| instanceNodeStartTime, | |||||
| instanceNodeStatus, | |||||
| }: LogListProps) { | |||||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||||
| useEffect(() => { | |||||
| if (workflowId) { | |||||
| const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | |||||
| const params = { | |||||
| task_id: pipelineNodeId, | |||||
| component_id: workflowId, | |||||
| name: instanceName, | |||||
| namespace: instanceNamespace, | |||||
| start_time: start_time, | |||||
| }; | |||||
| getExperimentLog(params, start_time); | |||||
| } | |||||
| }, [workflowId, instanceNodeStartTime]); | |||||
| // 获取实验日志 | |||||
| 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, | |||||
| }, | |||||
| ]; | |||||
| setLogList(list); | |||||
| } else if (log_type === 'resource') { | |||||
| const list = pods.map((v: string) => ({ | |||||
| log_type, | |||||
| pod_name: v, | |||||
| log_content: '', | |||||
| start_time, | |||||
| })); | |||||
| setLogList(list); | |||||
| } | |||||
| } | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className={styles['log-list']}> | |||||
| {list.length > 0 ? ( | |||||
| list.map((v) => <LogGroup key={v.pod_name} {...v} status={status} />) | |||||
| <div className={styles['log-list']} id="log-list"> | |||||
| {logList.length > 0 ? ( | |||||
| logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||||
| ) : ( | ) : ( | ||||
| <div className={styles['log-list__empty']}>暂无日志</div> | <div className={styles['log-list__empty']}>暂无日志</div> | ||||
| )} | )} | ||||
| @@ -42,7 +42,7 @@ const statusConfig: Record<TensorBoardStatus, TensorBoardStatusInfo> = { | |||||
| }; | }; | ||||
| type TensorBoardStatusProps = { | type TensorBoardStatusProps = { | ||||
| status: TensorBoardStatus; | |||||
| status?: TensorBoardStatus; | |||||
| onClick: () => void; | onClick: () => void; | ||||
| }; | }; | ||||
| @@ -1,31 +1,28 @@ | |||||
| import CommonTableCell from '@/components/CommonTableCell'; | import CommonTableCell from '@/components/CommonTableCell'; | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { TensorBoardStatus } from '@/enums'; | |||||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||||
| import { | import { | ||||
| deleteExperimentById, | deleteExperimentById, | ||||
| deleteQueryByExperimentInsId, | |||||
| getExperiment, | getExperiment, | ||||
| getExperimentById, | getExperimentById, | ||||
| getQueryByExperimentId, | getQueryByExperimentId, | ||||
| getTensorBoardStatusReq, | getTensorBoardStatusReq, | ||||
| postExperiment, | postExperiment, | ||||
| putExperiment, | putExperiment, | ||||
| putQueryByExperimentInsId, | |||||
| runExperiments, | runExperiments, | ||||
| runTensorBoardReq, | runTensorBoardReq, | ||||
| } from '@/services/experiment/index.js'; | } from '@/services/experiment/index.js'; | ||||
| import { getWorkflow } from '@/services/pipeline/index.js'; | import { getWorkflow } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, ConfigProvider, Dropdown, Space, Table, Tooltip } from 'antd'; | |||||
| import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||
| import { ComparisonType } from './Comparison'; | |||||
| import { ComparisonType } from './Comparison/config'; | |||||
| import AddExperimentModal from './components/AddExperimentModal'; | import AddExperimentModal from './components/AddExperimentModal'; | ||||
| import TensorBoardStatusCell from './components/TensorBoardStatus'; | |||||
| import ExperimentInstance from './components/ExperimentInstance'; | |||||
| import Styles from './index.less'; | import Styles from './index.less'; | ||||
| import { experimentStatusInfo } from './status'; | import { experimentStatusInfo } from './status'; | ||||
| @@ -49,7 +46,18 @@ function Experiment() { | |||||
| const [isAdd, setIsAdd] = useState(true); | const [isAdd, setIsAdd] = useState(true); | ||||
| const [isModalOpen, setIsModalOpen] = useState(false); | const [isModalOpen, setIsModalOpen] = useState(false); | ||||
| const [addFormData, setAddFormData] = useState({}); | const [addFormData, setAddFormData] = useState({}); | ||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const pageOption = useRef({ page: 1, size: 10 }); | |||||
| const paginationProps = { | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| total: total, | |||||
| page: pageOption.current.page, | |||||
| size: pageOption.current.size, | |||||
| onChange: (current, size) => paginationChange(current, size), | |||||
| }; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getList(); | getList(); | ||||
| @@ -58,6 +66,7 @@ function Experiment() { | |||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getList = async () => { | const getList = async () => { | ||||
| const params = { | const params = { | ||||
| @@ -76,6 +85,7 @@ function Experiment() { | |||||
| setTotal(res.data.totalElements); | setTotal(res.data.totalElements); | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取流水线列表 | // 获取流水线列表 | ||||
| const getWorkflowList = async () => { | const getWorkflowList = async () => { | ||||
| const [res, _] = await to(getWorkflow(queryFlow)); | const [res, _] = await to(getWorkflow(queryFlow)); | ||||
| @@ -83,39 +93,45 @@ function Experiment() { | |||||
| setWorkflowList(res.data.content); | setWorkflowList(res.data.content); | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取实验实例 | |||||
| const getQueryByExperiment = (val) => { | |||||
| getQueryByExperimentId(val).then((ret) => { | |||||
| setExpandedRowKeys(val); | |||||
| if (ret && ret.data && ret.data.length > 0) { | |||||
| try { | |||||
| const list = ret.data.map((v) => { | |||||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||||
| return { | |||||
| ...v, | |||||
| nodes_result, | |||||
| }; | |||||
| }); | |||||
| // 获取实验实例列表 | |||||
| const getQueryByExperiment = async (experimentId, page) => { | |||||
| const params = { | |||||
| experimentId: experimentId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const [res, error] = await to(getQueryByExperimentId(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setExpandedRowKeys(experimentId); | |||||
| try { | |||||
| const list = content.map((v) => { | |||||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||||
| return { | |||||
| ...v, | |||||
| nodes_result, | |||||
| }; | |||||
| }); | |||||
| if (page === 0) { | |||||
| setExperimentInList(list); | setExperimentInList(list); | ||||
| // 获取 TensorBoard 状态 | |||||
| list.forEach((item) => { | |||||
| if (item.nodes_result?.tensorboard_log) { | |||||
| const timerId = setTimeout(() => { | |||||
| getTensorBoardStatus(item); | |||||
| }, 0); | |||||
| timerIds.set(item.id, timerId); | |||||
| } | |||||
| }); | |||||
| } catch (error) { | |||||
| setExperimentInList([]); | |||||
| clearExperimentInTimers(); | |||||
| } else { | |||||
| setExperimentInList((prev) => [...prev, ...list]); | |||||
| } | } | ||||
| getList(); | |||||
| } else { | |||||
| setExperimentInList([]); | |||||
| getList(); | |||||
| setExperimentInsTotal(totalElements); | |||||
| // 获取 TensorBoard 状态 | |||||
| list.forEach((item) => { | |||||
| if (item.nodes_result?.tensorboard_log) { | |||||
| getTensorBoardStatus(item); | |||||
| } | |||||
| }); | |||||
| } catch (error) { | |||||
| console.log(error); | |||||
| } | } | ||||
| }); | |||||
| } | |||||
| }; | }; | ||||
| // 运行 TensorBoard | // 运行 TensorBoard | ||||
| const runTensorBoard = async (experimentIn) => { | const runTensorBoard = async (experimentIn) => { | ||||
| const params = { | const params = { | ||||
| @@ -134,6 +150,7 @@ function Experiment() { | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取 TensorBoard 状态 | // 获取 TensorBoard 状态 | ||||
| const getTensorBoardStatus = async (experimentIn) => { | const getTensorBoardStatus = async (experimentIn) => { | ||||
| const params = { | const params = { | ||||
| @@ -155,20 +172,30 @@ function Experiment() { | |||||
| return item; | return item; | ||||
| }); | }); | ||||
| }); | }); | ||||
| const timerId = setTimeout(() => { | |||||
| let timerId = timerIds.get(experimentIn.id); | |||||
| if (timerId) { | |||||
| clearTimeout(timerId); | |||||
| timerIds.delete(experimentIn.id); | |||||
| } | |||||
| timerId = setTimeout(() => { | |||||
| getTensorBoardStatus(experimentIn); | getTensorBoardStatus(experimentIn); | ||||
| }, 10000); | |||||
| }, 10 * 1000); | |||||
| timerIds.set(experimentIn.id, timerId); | timerIds.set(experimentIn.id, timerId); | ||||
| } | } | ||||
| }; | }; | ||||
| // 展开实例 | |||||
| const expandChange = (e, record) => { | const expandChange = (e, record) => { | ||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| setExperimentInList([]); | |||||
| if (record.id === expandedRowKeys) { | if (record.id === expandedRowKeys) { | ||||
| setExpandedRowKeys(null); | setExpandedRowKeys(null); | ||||
| } else { | } else { | ||||
| getQueryByExperiment(record.id); | |||||
| getQueryByExperiment(record.id, 0); | |||||
| } | } | ||||
| }; | }; | ||||
| // 终止实验实例获取 TensorBoard 状态的定时器 | // 终止实验实例获取 TensorBoard 状态的定时器 | ||||
| const clearExperimentInTimers = () => { | const clearExperimentInTimers = () => { | ||||
| timerIds.values().forEach((timerId) => { | timerIds.values().forEach((timerId) => { | ||||
| @@ -176,6 +203,7 @@ function Experiment() { | |||||
| }); | }); | ||||
| timerIds.clear(); | timerIds.clear(); | ||||
| }; | }; | ||||
| // 创建实验 | // 创建实验 | ||||
| const createExperiment = () => { | const createExperiment = () => { | ||||
| setIsAdd(true); | setIsAdd(true); | ||||
| @@ -183,6 +211,7 @@ function Experiment() { | |||||
| setExperimentId(null); | setExperimentId(null); | ||||
| setIsModalOpen(true); | setIsModalOpen(true); | ||||
| }; | }; | ||||
| // 编辑实验 | // 编辑实验 | ||||
| const editExperiment = (id) => { | const editExperiment = (id) => { | ||||
| getExperimentById(id).then((res) => { | getExperimentById(id).then((res) => { | ||||
| @@ -198,10 +227,7 @@ function Experiment() { | |||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| }; | }; | ||||
| const routeToEdit = (e, record) => { | |||||
| e.stopPropagation(); | |||||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||||
| }; | |||||
| // 创建或者编辑实验接口请求 | // 创建或者编辑实验接口请求 | ||||
| const handleAddExperiment = async (values) => { | const handleAddExperiment = async (values) => { | ||||
| const global_param = JSON.stringify(values.global_param); | const global_param = JSON.stringify(values.global_param); | ||||
| @@ -226,16 +252,7 @@ function Experiment() { | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| const pageOption = useRef({ page: 1, size: 10 }); | |||||
| const paginationProps = { | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| total: total, | |||||
| page: pageOption.current.page, | |||||
| size: pageOption.current.size, | |||||
| onChange: (current, size) => paginationChange(current, size), | |||||
| }; | |||||
| // 当前页面切换 | // 当前页面切换 | ||||
| const paginationChange = async (current, size) => { | const paginationChange = async (current, size) => { | ||||
| pageOption.current = { | pageOption.current = { | ||||
| @@ -244,21 +261,29 @@ function Experiment() { | |||||
| }; | }; | ||||
| getList(); | getList(); | ||||
| }; | }; | ||||
| const runExperiment = (id) => { | |||||
| runExperiments(id).then((ret) => { | |||||
| if (ret.code === 200) { | |||||
| message.success('运行成功'); | |||||
| getQueryByExperiment(id); | |||||
| } else { | |||||
| message.error('运行失败'); | |||||
| } | |||||
| }); | |||||
| // 运行实验 | |||||
| const runExperiment = async (id) => { | |||||
| const [res] = await to(runExperiments(id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| refreshExperimentIns(id); | |||||
| } else { | |||||
| message.error('运行失败'); | |||||
| } | |||||
| }; | }; | ||||
| const routerToText = (e, item, record) => { | |||||
| // 跳转到流水线 | |||||
| const gotoPipeline = (e, record) => { | |||||
| e.stopPropagation(); | e.stopPropagation(); | ||||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (item, record) => { | |||||
| navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | ||||
| }; | }; | ||||
| // 处理 TensorBoard 操作 | |||||
| const handleTensorboard = async (experimentIn) => { | const handleTensorboard = async (experimentIn) => { | ||||
| if ( | if ( | ||||
| experimentIn.tensorBoardStatus === TensorBoardStatus.Terminated || | experimentIn.tensorBoardStatus === TensorBoardStatus.Terminated || | ||||
| @@ -273,6 +298,21 @@ function Experiment() { | |||||
| } | } | ||||
| }; | }; | ||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIn) => { | |||||
| setExperimentInList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIn.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 实验对比菜单 | // 实验对比菜单 | ||||
| const getComparisonMenu = (experimentId) => { | const getComparisonMenu = (experimentId) => { | ||||
| return { | return { | ||||
| @@ -292,6 +332,17 @@ function Experiment() { | |||||
| }; | }; | ||||
| }; | }; | ||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId) => { | |||||
| getQueryByExperiment(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInList.length / 5); | |||||
| getQueryByExperiment(expandedRowKeys, page); | |||||
| }; | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| title: '实验名称', | title: '实验名称', | ||||
| @@ -304,7 +355,7 @@ function Experiment() { | |||||
| title: '关联流水线名称', | title: '关联流水线名称', | ||||
| dataIndex: 'workflow_name', | dataIndex: 'workflow_name', | ||||
| key: 'workflow_name', | key: 'workflow_name', | ||||
| render: (text, record) => <a onClick={(e) => routeToEdit(e, record)}>{text}</a>, | |||||
| render: (text, record) => <a onClick={(e) => gotoPipeline(e, record)}>{text}</a>, | |||||
| width: '16%', | width: '16%', | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -413,7 +464,7 @@ function Experiment() { | |||||
| ]; | ]; | ||||
| return ( | return ( | ||||
| <div className={Styles.experimentBox}> | <div className={Styles.experimentBox}> | ||||
| <div className={Styles.pipelineTopBox}> | |||||
| <div className={Styles.experimentTopBox}> | |||||
| <Button type="default" onClick={createExperiment} icon={<KFIcon type="icon-xinjian2" />}> | <Button type="default" onClick={createExperiment} icon={<KFIcon type="icon-xinjian2" />}> | ||||
| 新建实验 | 新建实验 | ||||
| </Button> | </Button> | ||||
| @@ -427,130 +478,16 @@ function Experiment() { | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | scroll={{ y: 'calc(100% - 55px)' }} | ||||
| expandable={{ | expandable={{ | ||||
| expandedRowRender: (record) => ( | expandedRowRender: (record) => ( | ||||
| <div> | |||||
| {experimentInList && experimentInList.length > 0 ? ( | |||||
| <div className={Styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||||
| <div className={Styles.index}>序号</div> | |||||
| <div className={Styles.tensorBoard}>可视化</div> | |||||
| <div className={Styles.description}> | |||||
| <div style={{ width: '50%' }}>运行时长</div> | |||||
| <div style={{ width: '50%' }}>开始时间</div> | |||||
| </div> | |||||
| <div className={Styles.status}>状态</div> | |||||
| <div className={Styles.operation}>操作</div> | |||||
| </div> | |||||
| ) : ( | |||||
| '' | |||||
| )} | |||||
| {experimentInList && experimentInList.length > 0 | |||||
| ? experimentInList.map((item, index) => ( | |||||
| <div | |||||
| key={item.id} | |||||
| className={classNames(Styles.tableExpandBox, Styles.tableExpandBoxContent)} | |||||
| > | |||||
| <a | |||||
| className={Styles.index} | |||||
| style={{ padding: '0 16px' }} | |||||
| onClick={(e) => routerToText(e, item, record)} | |||||
| > | |||||
| {index + 1} | |||||
| </a> | |||||
| <div className={Styles.tensorBoard}> | |||||
| {item.nodes_result?.tensorboard_log ? ( | |||||
| <TensorBoardStatusCell | |||||
| status={item.tensorBoardStatus} | |||||
| onClick={() => handleTensorboard(item)} | |||||
| ></TensorBoardStatusCell> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | |||||
| <div className={Styles.description}> | |||||
| <div style={{ width: '50%' }}> | |||||
| {elapsedTime(item.create_time, item.finish_time)} | |||||
| </div> | |||||
| <div style={{ width: '50%' }} className={Styles.startTime}> | |||||
| <Tooltip title={formatDate(item.create_time)}> | |||||
| <span>{formatDate(item.create_time)}</span> | |||||
| </Tooltip> | |||||
| </div> | |||||
| </div> | |||||
| <div className={Styles.statusBox}> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[item.status]?.icon} | |||||
| /> | |||||
| <span | |||||
| style={{ color: experimentStatusInfo[item.status]?.color }} | |||||
| className={Styles.statusIcon} | |||||
| > | |||||
| {experimentStatusInfo[item.status]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <div className={Styles.operation}> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="stop" | |||||
| disabled={ | |||||
| item.status === 'Succeeded' || | |||||
| item.status === 'Failed' || | |||||
| item.status === 'Terminated' | |||||
| } | |||||
| icon={<KFIcon type="icon-zhongzhi" />} | |||||
| onClick={async () => { | |||||
| putQueryByExperimentInsId(item.id).then((ret) => { | |||||
| if (ret.code === 200) { | |||||
| message.success('终止成功'); | |||||
| getQueryByExperiment(record.id); | |||||
| } else { | |||||
| message.error(ret.msg); | |||||
| } | |||||
| }); | |||||
| }} | |||||
| > | |||||
| 终止 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="batchRemove" | |||||
| disabled={item.status === 'Running' || item.status === 'Pending'} | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => { | |||||
| modalConfirm({ | |||||
| title: '确定删除该条实例吗?', | |||||
| onOk: () => { | |||||
| deleteQueryByExperimentInsId(item.id).then((ret) => { | |||||
| if (ret.code === 200) { | |||||
| message.success('删除成功'); | |||||
| getQueryByExperiment(record.id); | |||||
| } else { | |||||
| message.error(ret.msg); | |||||
| } | |||||
| }); | |||||
| }, | |||||
| }); | |||||
| }} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| </div> | |||||
| )) | |||||
| : ''} | |||||
| </div> | |||||
| <ExperimentInstance | |||||
| experimentInList={experimentInList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | |||||
| onClickTensorBoard={handleTensorboard} | |||||
| onRemove={() => refreshExperimentIns(record.id)} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | ), | ||||
| onExpand: (e, a) => { | onExpand: (e, a) => { | ||||
| expandChange(e, a); | expandChange(e, a); | ||||
| }, | }, | ||||
| @@ -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; | display: flex; | ||||
| flex: 1; | |||||
| align-items: center; | 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 { | .experimentTable { | ||||
| height: calc(100% - 60px); | height: calc(100% - 60px); | ||||
| :global { | :global { | ||||
| @@ -288,7 +288,13 @@ function MirrorInfo() { | |||||
| dataSource={tableData} | dataSource={tableData} | ||||
| columns={columns} | columns={columns} | ||||
| scroll={{ y: 'calc(100% - 55px)' }} | scroll={{ y: 'calc(100% - 55px)' }} | ||||
| pagination={{ ...pagination, total, showSizeChanger: true, showQuickJumper: true }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | onChange={handleTableChange} | ||||
| rowKey="id" | rowKey="id" | ||||
| /> | /> | ||||
| @@ -241,7 +241,7 @@ function MirrorList() { | |||||
| <div className={styles['mirror-list__content']}> | <div className={styles['mirror-list__content']}> | ||||
| <div className={styles['mirror-list__content__filter']}> | <div className={styles['mirror-list__content__filter']}> | ||||
| <Input.Search | <Input.Search | ||||
| placeholder="按数据集名称筛选" | |||||
| placeholder="按镜像名称筛选" | |||||
| allowClear | allowClear | ||||
| onSearch={onSearch} | onSearch={onSearch} | ||||
| onChange={(e) => setInputText(e.target.value)} | onChange={(e) => setInputText(e.target.value)} | ||||
| @@ -277,6 +277,7 @@ function MirrorList() { | |||||
| total: total, | total: total, | ||||
| showSizeChanger: true, | showSizeChanger: true, | ||||
| showQuickJumper: true, | showQuickJumper: true, | ||||
| showTotal: () => `共${total}条`, | |||||
| }} | }} | ||||
| onChange={handleTableChange} | onChange={handleTableChange} | ||||
| rowKey="id" | rowKey="id" | ||||
| @@ -1,9 +1,15 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-06-07 11:24:10 | |||||
| * @Description: 模型演化 | |||||
| */ | |||||
| import { useEffectWhen } from '@/hooks'; | import { useEffectWhen } from '@/hooks'; | ||||
| import { ResourceVersionData } from '@/pages/Dataset/config'; | import { ResourceVersionData } from '@/pages/Dataset/config'; | ||||
| import { getModelAtlasReq } from '@/services/dataset/index.js'; | import { getModelAtlasReq } from '@/services/dataset/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import G6, { G6GraphEvent, Graph } from '@antv/g6'; | |||||
| import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; | |||||
| // @ts-ignore | // @ts-ignore | ||||
| import { Flex, Select } from 'antd'; | import { Flex, Select } from 'antd'; | ||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||
| @@ -11,7 +17,15 @@ import GraphLegend from '../GraphLegend'; | |||||
| import NodeTooltips from '../NodeTooltips'; | import NodeTooltips from '../NodeTooltips'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils'; | 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 = { | type modeModelEvolutionProps = { | ||||
| resourceId: number; | resourceId: number; | ||||
| @@ -37,6 +51,8 @@ function ModelEvolution({ | |||||
| const [hoverNodeData, setHoverNodeData] = useState< | const [hoverNodeData, setHoverNodeData] = useState< | ||||
| ModelDepsData | ProjectDependency | TrainDataset | undefined | ModelDepsData | ProjectDependency | TrainDataset | undefined | ||||
| >(undefined); | >(undefined); | ||||
| const apiData = useRef<ModelDepsData | undefined>(undefined); // 接口返回的树形结构 | |||||
| const hierarchyNodes = useRef<ModelDepsData[]>([]); // 层级迭代树形结构,得到的节点列表 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| initGraph(); | initGraph(); | ||||
| @@ -111,18 +127,7 @@ function ModelEvolution({ | |||||
| }, | }, | ||||
| }, | }, | ||||
| modes: { | 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) => { | 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 = nodeItem.getModel() as ModelDepsData | ProjectDependency | TrainDataset; | ||||
| const { model_type } = model; | 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 | // 鼠标滚轮缩放时,隐藏 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 = () => { | const handleTooltipsMouseEnter = () => { | ||||
| setEnterTooltip(true); | setEnterTooltip(true); | ||||
| }; | }; | ||||
| @@ -192,7 +223,9 @@ function ModelEvolution({ | |||||
| const [res] = await to(getModelAtlasReq(params)); | const [res] = await to(getModelAtlasReq(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const data = normalizeTreeData(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.data(graphData); | ||||
| graph.render(); | graph.render(); | ||||
| @@ -6,21 +6,22 @@ import Hierarchy from '@antv/hierarchy'; | |||||
| export const nodeWidth = 90; | export const nodeWidth = 90; | ||||
| export const nodeHeight = 40; | export const nodeHeight = 40; | ||||
| export const vGap = nodeHeight + 20; | export const vGap = nodeHeight + 20; | ||||
| export const hGap = nodeWidth; | |||||
| export const hGap = nodeHeight + 20; | |||||
| export const ellipseWidth = nodeWidth; | export const ellipseWidth = nodeWidth; | ||||
| export const labelPadding = 30; | export const labelPadding = 30; | ||||
| export const nodeFontSize = 8; | export const nodeFontSize = 8; | ||||
| export const datasetHGap = 20; | |||||
| // 数据集节点 | // 数据集节点 | ||||
| const datasetNodes: NodeConfig[] = []; | const datasetNodes: NodeConfig[] = []; | ||||
| export enum NodeType { | 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 = { | export type Rect = { | ||||
| @@ -40,14 +41,14 @@ export interface TrainDataset extends NodeConfig { | |||||
| dataset_id: number; | dataset_id: number; | ||||
| dataset_name: string; | dataset_name: string; | ||||
| dataset_version: string; | dataset_version: string; | ||||
| model_type: NodeType.testDataset | NodeType.trainDataset; | |||||
| model_type: NodeType.TestDataset | NodeType.TrainDataset; | |||||
| } | } | ||||
| export interface ProjectDependency extends NodeConfig { | export interface ProjectDependency extends NodeConfig { | ||||
| url: string; | url: string; | ||||
| name: string; | name: string; | ||||
| branch: string; | branch: string; | ||||
| model_type: NodeType.project; | |||||
| model_type: NodeType.Project; | |||||
| } | } | ||||
| export type ModalDetail = { | export type ModalDetail = { | ||||
| @@ -66,9 +67,9 @@ export interface ModelDepsAPIData { | |||||
| version: string; | version: string; | ||||
| workflow_id: number; | workflow_id: number; | ||||
| exp_ins_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; | current_model_name: string; | ||||
| project_dependency: ProjectDependency; | |||||
| project_dependency?: ProjectDependency; | |||||
| test_dataset: TrainDataset[]; | test_dataset: TrainDataset[]; | ||||
| train_dataset: TrainDataset[]; | train_dataset: TrainDataset[]; | ||||
| train_task: TrainTask; | train_task: TrainTask; | ||||
| @@ -79,16 +80,22 @@ export interface ModelDepsAPIData { | |||||
| export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData { | export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData { | ||||
| children: ModelDepsData[]; | children: ModelDepsData[]; | ||||
| expanded: boolean; // 是否展开 | |||||
| level: number; // 层级,从 0 开始 | |||||
| datasetLen: number; // 数据集数量 | |||||
| } | } | ||||
| // 规范化子数据 | // 规范化子数据 | ||||
| export function normalizeChildren(data: ModelDepsData[]) { | export function normalizeChildren(data: ModelDepsData[]) { | ||||
| if (Array.isArray(data)) { | if (Array.isArray(data)) { | ||||
| data.forEach((item) => { | 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.id = `$M_${item.current_model_id}_${item.version}`; | ||||
| item.label = getLabel(item); | item.label = getLabel(item); | ||||
| item.style = getStyle(NodeType.children); | |||||
| item.style = getStyle(NodeType.Children); | |||||
| normalizeChildren(item.children); | normalizeChildren(item.children); | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -111,22 +118,22 @@ export function getLabel(node: ModelDepsData | ModelDepsAPIData) { | |||||
| export function getStyle(model_type: NodeType) { | export function getStyle(model_type: NodeType) { | ||||
| let fill = ''; | let fill = ''; | ||||
| switch (model_type) { | switch (model_type) { | ||||
| case NodeType.current: | |||||
| case NodeType.Current: | |||||
| fill = 'l(0) 0:#72a1ff 1:#1664ff'; | fill = 'l(0) 0:#72a1ff 1:#1664ff'; | ||||
| break; | break; | ||||
| case NodeType.parent: | |||||
| case NodeType.Parent: | |||||
| fill = 'l(0) 0:#93dfd1 1:#43c9b1'; | fill = 'l(0) 0:#93dfd1 1:#43c9b1'; | ||||
| break; | break; | ||||
| case NodeType.children: | |||||
| case NodeType.Children: | |||||
| fill = 'l(0) 0:#72b4ff 1:#169aff'; | fill = 'l(0) 0:#72b4ff 1:#169aff'; | ||||
| break; | break; | ||||
| case NodeType.project: | |||||
| case NodeType.Project: | |||||
| fill = 'l(0) 0:#b3a9ff 1:#8981ff'; | fill = 'l(0) 0:#b3a9ff 1:#8981ff'; | ||||
| break; | break; | ||||
| case NodeType.trainDataset: | |||||
| case NodeType.TrainDataset: | |||||
| fill = '#a5d878'; | fill = '#a5d878'; | ||||
| break; | break; | ||||
| case NodeType.testDataset: | |||||
| case NodeType.TestDataset: | |||||
| fill = '#d8b578'; | fill = '#d8b578'; | ||||
| break; | break; | ||||
| default: | default: | ||||
| @@ -145,11 +152,15 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||||
| }) as ModelDepsData; | }) as ModelDepsData; | ||||
| // 设置当前模型的数据 | // 设置当前模型的数据 | ||||
| normalizedData.model_type = NodeType.current; | |||||
| normalizedData.model_type = NodeType.Current; | |||||
| normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`; | normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`; | ||||
| normalizedData.label = getLabel(normalizedData); | 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[]); | normalizeChildren(normalizedData.children as ModelDepsData[]); | ||||
| normalizedData.level = 0; | |||||
| // 将 parent_models 转换成树形结构 | // 将 parent_models 转换成树形结构 | ||||
| let parent_models = normalizedData.parent_models || []; | let parent_models = normalizedData.parent_models || []; | ||||
| @@ -157,10 +168,13 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||||
| const parent = parent_models[0]; | const parent = parent_models[0]; | ||||
| normalizedData = { | normalizedData = { | ||||
| ...parent, | ...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}`, | id: `$M_${parent.current_model_id}_${parent.version}`, | ||||
| label: getLabel(parent), | label: getLabel(parent), | ||||
| style: getStyle(NodeType.parent), | |||||
| style: getStyle(NodeType.Parent), | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| ...normalizedData, | ...normalizedData, | ||||
| @@ -174,13 +188,34 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||||
| } | } | ||||
| // 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据 | // 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据 | ||||
| export function getGraphData(data: ModelDepsData): GraphData { | |||||
| export function getGraphData(data: ModelDepsData, hierarchyNodes: ModelDepsData[]): GraphData { | |||||
| const config = { | const config = { | ||||
| direction: 'LR', | direction: 'LR', | ||||
| getHeight: () => nodeHeight, | getHeight: () => nodeHeight, | ||||
| getWidth: () => nodeWidth, | 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) => { | Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => { | ||||
| const data = node.data as ModelDepsData; | const data = node.data as ModelDepsData; | ||||
| // 当前模型显示数据集和项目 | // 当前模型显示数据集和项目 | ||||
| if (data.model_type === NodeType.current) { | |||||
| if (data.expanded === true) { | |||||
| addDatasetDependency(data, node, nodes, edges); | addDatasetDependency(data, node, nodes, edges); | ||||
| addProjectDependency(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({ | nodes.push({ | ||||
| ...data, | ...data, | ||||
| @@ -219,16 +254,16 @@ const addDatasetDependency = ( | |||||
| nodes: NodeConfig[], | nodes: NodeConfig[], | ||||
| edges: EdgeConfig[], | edges: EdgeConfig[], | ||||
| ) => { | ) => { | ||||
| const { train_dataset, test_dataset } = data; | |||||
| const { train_dataset, test_dataset, id } = data; | |||||
| train_dataset.forEach((item) => { | 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) => { | 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; | datasetNodes.length = 0; | ||||
| @@ -243,7 +278,7 @@ const addDatasetDependency = ( | |||||
| fittingString(node.dataset_version, ellipseWidth - labelPadding, nodeFontSize); | fittingString(node.dataset_version, ellipseWidth - labelPadding, nodeFontSize); | ||||
| const half = len / 2 - 0.5; | 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; | node.y = currentNode.y! - nodeHeight - vGap; | ||||
| nodes.push(node); | nodes.push(node); | ||||
| datasetNodes.push(node); | datasetNodes.push(node); | ||||
| @@ -264,14 +299,14 @@ const addProjectDependency = ( | |||||
| nodes: NodeConfig[], | nodes: NodeConfig[], | ||||
| edges: EdgeConfig[], | edges: EdgeConfig[], | ||||
| ) => { | ) => { | ||||
| const { project_dependency } = data; | |||||
| const { project_dependency, id } = data; | |||||
| if (project_dependency?.url) { | if (project_dependency?.url) { | ||||
| const node = { ...project_dependency }; | 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.type = 'rect'; | ||||
| node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize); | node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize); | ||||
| node.style = getStyle(NodeType.project); | |||||
| node.style = getStyle(NodeType.Project); | |||||
| node.style.radius = nodeHeight / 2; | node.style.radius = nodeHeight / 2; | ||||
| node.x = currentNode.x; | node.x = currentNode.x; | ||||
| node.y = currentNode.y! + nodeHeight + vGap; | 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; | |||||
| } | |||||
| @@ -22,7 +22,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||||
| }; | }; | ||||
| const gotoModelPage = () => { | const gotoModelPage = () => { | ||||
| if (data.model_type === NodeType.current) { | |||||
| if (data.model_type === NodeType.Current) { | |||||
| return; | return; | ||||
| } | } | ||||
| if (data.current_model_id === resourceId) { | if (data.current_model_id === resourceId) { | ||||
| @@ -39,7 +39,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||||
| <div> | <div> | ||||
| <div className={styles['node-tooltips__row']}> | <div className={styles['node-tooltips__row']}> | ||||
| <span className={styles['node-tooltips__row__title']}>模型名称:</span> | <span className={styles['node-tooltips__row__title']}>模型名称:</span> | ||||
| {data.model_type === NodeType.current ? ( | |||||
| {data.model_type === NodeType.Current ? ( | |||||
| <span className={styles['node-tooltips__row__value']}> | <span className={styles['node-tooltips__row__value']}> | ||||
| {data.model_version_dependcy_vo?.name || '--'} | {data.model_version_dependcy_vo?.name || '--'} | ||||
| </span> | </span> | ||||
| @@ -199,14 +199,14 @@ function NodeTooltips({ | |||||
| if (!data) return null; | if (!data) return null; | ||||
| let Component = null; | let Component = null; | ||||
| const { model_type } = data; | const { model_type } = data; | ||||
| if (model_type === NodeType.testDataset || model_type === NodeType.trainDataset) { | |||||
| if (model_type === NodeType.TestDataset || model_type === NodeType.TrainDataset) { | |||||
| Component = <DatasetInfo data={data} />; | Component = <DatasetInfo data={data} />; | ||||
| } else if (model_type === NodeType.project) { | |||||
| } else if (model_type === NodeType.Project) { | |||||
| Component = <ProjectInfo data={data} />; | Component = <ProjectInfo data={data} />; | ||||
| } else if ( | } 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 = <ModelInfo resourceId={resourceId} data={data} onVersionChange={onVersionChange} />; | Component = <ModelInfo resourceId={resourceId} data={data} onVersionChange={onVersionChange} />; | ||||
| } | } | ||||
| @@ -5,22 +5,20 @@ | |||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import ParameterInput from '@/components/ParameterInput'; | |||||
| import ResourceSelect, { | |||||
| requiredValidator, | |||||
| type ParameterInputObject, | |||||
| } from '@/components/ResourceSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import ResourceSelectorModal, { | |||||
| ResourceSelectorResponse, | |||||
| ResourceSelectorType, | |||||
| selectorTypeConfig, | |||||
| } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||||
| import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||||
| import { | import { | ||||
| createModelDeploymentReq, | createModelDeploymentReq, | ||||
| restartModelDeploymentReq, | restartModelDeploymentReq, | ||||
| updateModelDeploymentReq, | updateModelDeploymentReq, | ||||
| } from '@/services/modelDeployment'; | } from '@/services/modelDeployment'; | ||||
| import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils'; | import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils'; | ||||
| import { openAntdModal } from '@/utils/modal'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { | import { | ||||
| getSessionStorageItem, | getSessionStorageItem, | ||||
| @@ -39,13 +37,8 @@ import styles from './index.less'; | |||||
| export type FormData = { | export type FormData = { | ||||
| serviceName: string; // 服务名称 | serviceName: string; // 服务名称 | ||||
| description: string; // 描述 | description: string; // 描述 | ||||
| model: { | |||||
| id: number; | |||||
| version: string; | |||||
| value: string; | |||||
| showValue: string; | |||||
| }; // 模型 | |||||
| image: string; // 镜像 | |||||
| model: ParameterInputObject; // 模型 | |||||
| image: ParameterInputObject; // 镜像 | |||||
| resource: string; // 资源规格 | resource: string; // 资源规格 | ||||
| replicas: string; // 副本数量 | replicas: string; // 副本数量 | ||||
| modelPath: string; // 模型路径 | modelPath: string; // 模型路径 | ||||
| @@ -56,16 +49,10 @@ function ModelDeploymentCreate() { | |||||
| const navgite = useNavigate(); | const navgite = useNavigate(); | ||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | const [resourceStandardList, filterResourceStandard] = useComputingResource(); | ||||
| const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>( | |||||
| undefined, | |||||
| ); // 选择的模型,为了再次打开时恢复原来的选择 | |||||
| const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create); | const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create); | ||||
| const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>( | const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>( | ||||
| undefined, | undefined, | ||||
| ); | ); | ||||
| const [selectedMirror, setSelectedMirror] = useState<ResourceSelectorResponse | undefined>( | |||||
| undefined, | |||||
| ); // 选择的镜像,为了再次打开时恢复原来的选择 | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -81,65 +68,23 @@ function ModelDeploymentCreate() { | |||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| // 获取选择数据集、模型后面按钮 icon | |||||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | |||||
| return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | |||||
| }; | |||||
| // 选择模型、镜像 | |||||
| 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 createModelDeployment = async (formData: FormData) => { | ||||
| const envList = formData['env'] ?? []; | const envList = formData['env'] ?? []; | ||||
| const image = formData['image']; | |||||
| const model = formData['model']; | |||||
| const env = envList.reduce((acc, cur) => { | const env = envList.reduce((acc, cur) => { | ||||
| acc[cur.key] = cur.value; | acc[cur.key] = cur.value; | ||||
| return acc; | return acc; | ||||
| }, {} as Record<string, string>); | }, {} as Record<string, string>); | ||||
| // 根据后台要求,修改表单数据 | |||||
| const object = camelCaseToUnderscore({ | const object = camelCaseToUnderscore({ | ||||
| ...omit(formData, ['replicas', 'env']), | |||||
| ...omit(formData, ['replicas', 'env', 'image', 'model']), | |||||
| replicas: Number(formData.replicas), | replicas: Number(formData.replicas), | ||||
| env, | env, | ||||
| image: image.value, | |||||
| model: pick(model, ['id', 'version', 'path', 'showValue']), | |||||
| }); | }); | ||||
| const params = | const params = | ||||
| @@ -258,31 +203,21 @@ function ModelDeploymentCreate() { | |||||
| name="model" | name="model" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | |||||
| validator: requiredValidator, | |||||
| message: '请选择模型', | message: '请选择模型', | ||||
| }, | }, | ||||
| ]} | ]} | ||||
| required | |||||
| > | > | ||||
| <ParameterInput | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Model} | |||||
| placeholder="请选择模型" | placeholder="请选择模型" | ||||
| disabled={disabled} | disabled={disabled} | ||||
| canInput={false} | canInput={false} | ||||
| size="large" | size="large" | ||||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| <Col span={10}> | |||||
| <Button | |||||
| disabled={disabled} | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(ResourceSelectorType.Model)} | |||||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||||
| > | |||||
| 选择模型 | |||||
| </Button> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| @@ -291,29 +226,20 @@ function ModelDeploymentCreate() { | |||||
| name="image" | name="image" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | |||||
| message: '请输入镜像', | |||||
| validator: requiredValidator, | |||||
| message: '请选择镜像', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| required | |||||
| > | > | ||||
| <ParameterInput | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Mirror} | |||||
| placeholder="请选择镜像" | placeholder="请选择镜像" | ||||
| canInput={false} | canInput={false} | ||||
| size="large" | size="large" | ||||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| <Col span={10}> | |||||
| <Button | |||||
| size="large" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(ResourceSelectorType.Mirror)} | |||||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||||
| > | |||||
| 选择镜像 | |||||
| </Button> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| @@ -223,7 +223,7 @@ function ModelDeployment() { | |||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| dataIndex: 'operation', | dataIndex: 'operation', | ||||
| width: 350, | |||||
| width: 250, | |||||
| key: 'operation', | key: 'operation', | ||||
| render: (_: any, record: ModelDeploymentData) => ( | render: (_: any, record: ModelDeploymentData) => ( | ||||
| <div> | <div> | ||||
| @@ -336,6 +336,7 @@ function ModelDeployment() { | |||||
| total: total, | total: total, | ||||
| showSizeChanger: true, | showSizeChanger: true, | ||||
| showQuickJumper: true, | showQuickJumper: true, | ||||
| showTotal: () => `共${total}条`, | |||||
| }} | }} | ||||
| onChange={handleTableChange} | onChange={handleTableChange} | ||||
| rowKey="service_id" | rowKey="service_id" | ||||
| @@ -37,6 +37,7 @@ export type SelectorTypeInfo = { | |||||
| litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 | litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 | ||||
| fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 | fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 | ||||
| tabItems: TabsProps['items']; // tab 列表 | tabItems: TabsProps['items']; // tab 列表 | ||||
| buttontTitle: string; // 按钮 title | |||||
| }; | }; | ||||
| // 获取镜像文件列表,为了兼容数据集和模型 | // 获取镜像文件列表,为了兼容数据集和模型 | ||||
| @@ -77,6 +78,7 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||||
| label: '公开模型', | label: '公开模型', | ||||
| }, | }, | ||||
| ], | ], | ||||
| buttontTitle: '选择模型', | |||||
| }, | }, | ||||
| [ResourceSelectorType.Dataset]: { | [ResourceSelectorType.Dataset]: { | ||||
| getList: getDatasetList, | getList: getDatasetList, | ||||
| @@ -98,6 +100,7 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||||
| label: '公开数据集', | label: '公开数据集', | ||||
| }, | }, | ||||
| ], | ], | ||||
| buttontTitle: '选择数据集', | |||||
| }, | }, | ||||
| [ResourceSelectorType.Mirror]: { | [ResourceSelectorType.Mirror]: { | ||||
| getList: getMirrorListReq, | getList: getMirrorListReq, | ||||
| @@ -121,5 +124,6 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||||
| label: '公开镜像', | label: '公开镜像', | ||||
| }, | }, | ||||
| ], | ], | ||||
| buttontTitle: '选择镜像', | |||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -39,7 +39,7 @@ export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| defaultExpandedKeys?: React.Key[]; | defaultExpandedKeys?: React.Key[]; | ||||
| defaultCheckedKeys?: React.Key[]; | defaultCheckedKeys?: React.Key[]; | ||||
| defaultActiveTab?: CommonTabKeys; | defaultActiveTab?: CommonTabKeys; | ||||
| onOk?: (params: ResourceSelectorResponse | null) => void; | |||||
| onOk?: (params: ResourceSelectorResponse | undefined) => void; | |||||
| } | } | ||||
| type TreeRef = GetRef<typeof Tree<TreeDataNode>>; | type TreeRef = GetRef<typeof Tree<TreeDataNode>>; | ||||
| @@ -279,7 +279,7 @@ function ResourceSelectorModal({ | |||||
| }; | }; | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } else { | } else { | ||||
| onOk?.(null); | |||||
| onOk?.(undefined); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -86,30 +86,42 @@ const EditPipeline = () => { | |||||
| // 保存 | // 保存 | ||||
| const savePipeline = async (val) => { | 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('全局参数配置有误'); | message.error('全局参数配置有误'); | ||||
| openParamsDrawer(); | openParamsDrawer(); | ||||
| return; | return; | ||||
| } | } | ||||
| closeParamsDrawer(); | |||||
| const [propsRes, propsError] = await to(propsRef.current.getFieldsValue()); | |||||
| const [propsRes, propsError] = await to(propsRef.current.validateFields()); | |||||
| if (propsError) { | if (propsError) { | ||||
| message.error('节点必填项必须配置'); | message.error('节点必填项必须配置'); | ||||
| return; | return; | ||||
| } | } | ||||
| propsRef.current.propClose(); | |||||
| propsRef.current.close(); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| const data = graph.save(); | const data = graph.save(); | ||||
| console.log(data); | 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 = { | const params = { | ||||
| ...locationParams, | ...locationParams, | ||||
| dag: JSON.stringify(data), | dag: JSON.stringify(data), | ||||
| global_param: JSON.stringify(res.global_param), | |||||
| global_param: JSON.stringify(globalParamRes.global_param), | |||||
| }; | }; | ||||
| saveWorkflow(params).then((ret) => { | saveWorkflow(params).then((ret) => { | ||||
| message.success('保存成功'); | message.success('保存成功'); | ||||
| closeParamsDrawer(); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| if (val) { | if (val) { | ||||
| navgite({ pathname: `/pipeline/template` }); | navgite({ pathname: `/pipeline/template` }); | ||||
| @@ -122,10 +134,6 @@ const EditPipeline = () => { | |||||
| // 渲染数据 | // 渲染数据 | ||||
| const getGraphData = (data) => { | const getGraphData = (data) => { | ||||
| if (graph) { | if (graph) { | ||||
| // 修改历史数据有蓝色边框的问题 | |||||
| data.nodes.forEach((item) => { | |||||
| item.style.stroke = '#fff'; | |||||
| }); | |||||
| graph.data(data); | graph.data(data); | ||||
| graph.render(); | graph.render(); | ||||
| } else { | } 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 initGraph = () => { | ||||
| const contextMenu = initMenu(); | const contextMenu = initMenu(); | ||||
| @@ -302,7 +321,7 @@ const EditPipeline = () => { | |||||
| ); | ); | ||||
| }, | }, | ||||
| afterDraw(cfg, group) { | afterDraw(cfg, group) { | ||||
| const image = group.addShape('image', { | |||||
| group.addShape('image', { | |||||
| attrs: { | attrs: { | ||||
| x: -45, | x: -45, | ||||
| y: -10, | y: -10, | ||||
| @@ -329,7 +348,26 @@ const EditPipeline = () => { | |||||
| draggable: true, | 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(); | const bbox = group.getBBox(); | ||||
| if (cfg.formError) { | |||||
| bbox.y += 6; | |||||
| bbox.width -= 6; | |||||
| bbox.height -= 6; | |||||
| } | |||||
| const anchorPoints = this.getAnchorPoints(cfg); | const anchorPoints = this.getAnchorPoints(cfg); | ||||
| anchorPoints.forEach((anchorPos, i) => { | anchorPoints.forEach((anchorPos, i) => { | ||||
| group.addShape('circle', { | group.addShape('circle', { | ||||
| @@ -349,18 +387,10 @@ const EditPipeline = () => { | |||||
| draggable: true, | draggable: true, | ||||
| }); | }); | ||||
| }); | }); | ||||
| return image; | |||||
| }, | }, | ||||
| // response the state changes and show/hide the link-point circles | // response the state changes and show/hide the link-point circles | ||||
| setState(name, value, item) { | 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 group = item.getContainer(); | ||||
| const shape = group.get('children')[0]; | const shape = group.get('children')[0]; | ||||
| const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point'); | const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point'); | ||||
| @@ -371,7 +401,7 @@ const EditPipeline = () => { | |||||
| point.show(); | point.show(); | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| shape.attr('stroke', '#fff'); | |||||
| shape.attr('stroke', 'transparent'); | |||||
| anchorPoints.forEach((point) => { | anchorPoints.forEach((point) => { | ||||
| point.hide(); | point.hide(); | ||||
| }); | }); | ||||
| @@ -467,9 +497,13 @@ const EditPipeline = () => { | |||||
| }, | }, | ||||
| style: { | style: { | ||||
| fill: '#fff', | fill: '#fff', | ||||
| stroke: '#fff', | |||||
| stroke: 'transparent', | |||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| radius: 10, | |||||
| radius: 8, | |||||
| shadowColor: 'rgba(75, 84, 137, 0.4)', | |||||
| shadowBlur: 6, | |||||
| shadowOffsetX: 0, | |||||
| shadowOffsetY: 0, | |||||
| overflow: 'hidden', | overflow: 'hidden', | ||||
| lineWidth: 0.5, | lineWidth: 0.5, | ||||
| }, | }, | ||||
| @@ -500,19 +534,25 @@ const EditPipeline = () => { | |||||
| }, | }, | ||||
| }); | }); | ||||
| // 修改历史数据样式问题 | |||||
| graph.node((node) => { | |||||
| return { | |||||
| style: { | |||||
| stroke: 'transparent', | |||||
| radius: 8, | |||||
| }, | |||||
| }; | |||||
| }); | |||||
| // 绑定事件 | |||||
| bindEvents(); | bindEvents(); | ||||
| }; | }; | ||||
| // 绑定事件 | |||||
| const bindEvents = () => { | const bindEvents = () => { | ||||
| graph.on('node:click', (e) => { | graph.on('node:click', (e) => { | ||||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | 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) => { | graph.on('aftercreateedge', (e) => { | ||||
| @@ -599,36 +639,32 @@ const EditPipeline = () => { | |||||
| // 上下文菜单 | // 上下文菜单 | ||||
| const initMenu = () => { | const initMenu = () => { | ||||
| const contextMenu = new G6.Menu({ | const contextMenu = new G6.Menu({ | ||||
| className: 'pipeline-context-menu', | |||||
| getContent(evt) { | getContent(evt) { | ||||
| const type = evt.item.getType(); | const type = evt.item.getType(); | ||||
| const cloneDisplay = type === 'node' ? 'block' : 'none'; | |||||
| const cloneDisplay = type === 'node' ? 'flex' : 'none'; | |||||
| return ` | return ` | ||||
| <ul style="position: absolute; | |||||
| width: 100px; | |||||
| padding-left:0; | |||||
| display:flex; | |||||
| flex-direction: column; | |||||
| align-items:center; | |||||
| left: 0px; | |||||
| top: 0px; | |||||
| background-color: #ffffff; | |||||
| font-size: 14px; | |||||
| color: #333333; | |||||
| overflow-y: auto;"> | |||||
| <li style="padding: 10px 20px;cursor: pointer; display: ${cloneDisplay}" code="clone">复制</li> | |||||
| <li style="padding: 10px 20px;cursor: pointer;" code="delete">删除</li> | |||||
| </ul>`; | |||||
| <div> | |||||
| <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone"> | |||||
| <svg class="pipeline-context-menu__item__icon" id="clone-svg"> | |||||
| <use xlink:href="#icon-fuzhi1" /> | |||||
| </svg> | |||||
| 复制 | |||||
| </div> | |||||
| <div class="pipeline-context-menu__item" id="delete"> | |||||
| <svg class="pipeline-context-menu__item__icon" id="delete-svg"> | |||||
| <use xlink:href="#icon-shanchu1" /> | |||||
| </svg> | |||||
| 删除 | |||||
| </div> | |||||
| </div>`; | |||||
| }, | }, | ||||
| handleMenuClick: (target, item) => { | 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 | // offsetX and offsetY include the padding of the parent container | ||||
| @@ -685,7 +721,7 @@ const EditPipeline = () => { | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div> | <div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div> | ||||
| </div> | </div> | ||||
| <Props ref={propsRef} onParentChange={handleFormChange}></Props> | |||||
| <Props ref={propsRef} onFormChange={handleFormChange}></Props> | |||||
| <GlobalParamsDrawer | <GlobalParamsDrawer | ||||
| ref={paramsDrawerRef} | ref={paramsDrawerRef} | ||||
| open={paramsDrawerOpen} | open={paramsDrawerOpen} | ||||
| @@ -28,3 +28,37 @@ | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| :global { | |||||
| .pipeline-context-menu { | |||||
| width: 78px; | |||||
| padding: 10px 0; | |||||
| background: #ffffff; | |||||
| border-radius: 6px; | |||||
| box-shadow: 0px 0px 6px rgba(40, 84, 168, 0.21); | |||||
| &__item { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| width: 100%; | |||||
| height: 34px; | |||||
| padding-left: 12px; | |||||
| color: @text-color-secondary; | |||||
| font-size: 15px; | |||||
| cursor: pointer; | |||||
| &:hover { | |||||
| color: #0d5ef8; | |||||
| font-weight: 500; | |||||
| background-color: .addAlpha(#8895a8, 0.11) []; | |||||
| } | |||||
| &__icon { | |||||
| width: 1em; | |||||
| height: 1em; | |||||
| margin-right: 9px; | |||||
| fill: currentColor; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,11 +1,12 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import ParameterInput from '@/components/ParameterInput'; | |||||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||||
| import ParameterSelect from '@/components/ParameterSelect'; | import ParameterSelect from '@/components/ParameterSelect'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import { | import { | ||||
| PipelineGlobalParam, | PipelineGlobalParam, | ||||
| PipelineNodeModel, | |||||
| PipelineNodeModelParameter, | PipelineNodeModelParameter, | ||||
| PipelineNodeModelSerialize, | PipelineNodeModelSerialize, | ||||
| } from '@/types'; | } from '@/types'; | ||||
| @@ -25,10 +26,10 @@ import { canInput, createMenuItems } from './utils'; | |||||
| const { TextArea } = Input; | const { TextArea } = Input; | ||||
| type PipelineNodeParameterProps = { | type PipelineNodeParameterProps = { | ||||
| onParentChange: (data: PipelineNodeModelSerialize) => void; | |||||
| onFormChange: (data: PipelineNodeModelSerialize) => void; | |||||
| }; | }; | ||||
| const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParameterProps, ref) => { | |||||
| const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => { | |||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | ||||
| {} as PipelineNodeModelSerialize, | {} as PipelineNodeModelSerialize, | ||||
| @@ -37,19 +38,27 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | ||||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | ||||
| const afterOpenChange = () => { | |||||
| const afterOpenChange = async () => { | |||||
| if (!open) { | 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, | ...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 = () => { | const onClose = () => { | ||||
| @@ -57,45 +66,53 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| }; | }; | ||||
| useImperativeHandle(ref, () => ({ | 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(); | 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(); | 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; | type = ResourceSelectorType.Mirror; | ||||
| break; | break; | ||||
| } | } | ||||
| const fieldValue = form.getFieldValue(formItemName); | const fieldValue = form.getFieldValue(formItemName); | ||||
| const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | ||||
| const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | ||||
| @@ -162,8 +178,21 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| }); | }); | ||||
| } | } | ||||
| } else { | } 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(); | 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( | const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | ||||
| ([key, value]) => ({ key, value }), | ([key, value]) => ({ key, value }), | ||||
| @@ -232,7 +273,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| <Drawer | <Drawer | ||||
| title="编辑任务" | title="编辑任务" | ||||
| placement="right" | placement="right" | ||||
| rootStyle={{ marginTop: '45px' }} | |||||
| rootStyle={{ marginTop: '52px' }} | |||||
| getContainer={false} | getContainer={false} | ||||
| closeIcon={false} | closeIcon={false} | ||||
| onClose={onClose} | onClose={onClose} | ||||
| @@ -255,6 +296,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| maxWidth: 600, | maxWidth: 600, | ||||
| }} | }} | ||||
| autoComplete="off" | autoComplete="off" | ||||
| scrollToFirstError | |||||
| > | > | ||||
| <div className={styles['pipeline-drawer__title']}> | <div className={styles['pipeline-drawer__title']}> | ||||
| <SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle> | <SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle> | ||||
| @@ -351,7 +393,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| value: 'standard', | value: 'standard', | ||||
| }} | }} | ||||
| showSearch | showSearch | ||||
| allowClear | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item | <Form.Item | ||||
| @@ -382,11 +423,14 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| > | > | ||||
| <TextArea placeholder="请输入环境变量" allowClear /> | <TextArea placeholder="请输入环境变量" allowClear /> | ||||
| </Form.Item> | </Form.Item> | ||||
| {/* 控制参数 */} | |||||
| {controlStrategyList.map((item) => ( | {controlStrategyList.map((item) => ( | ||||
| <Form.Item | <Form.Item | ||||
| key={item.key} | key={item.key} | ||||
| name={['control_strategy', item.key]} | name={['control_strategy', item.key]} | ||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'control_strategy')} | label={getLabel(item, 'control_strategy')} | ||||
| rules={getFormRules(item)} | |||||
| > | > | ||||
| <ParameterInput allowClear></ParameterInput> | <ParameterInput allowClear></ParameterInput> | ||||
| </Form.Item> | </Form.Item> | ||||
| @@ -401,11 +445,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| required={item.value.require ? true : false} | required={item.value.require ? true : false} | ||||
| > | > | ||||
| <div className={styles['pipeline-drawer__ref-row']}> | <div className={styles['pipeline-drawer__ref-row']}> | ||||
| <Form.Item | |||||
| name={['in_parameters', item.key]} | |||||
| noStyle | |||||
| rules={[{ required: item.value.require ? true : false }]} | |||||
| > | |||||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||||
| {item.value.type === 'select' ? ( | {item.value.type === 'select' ? ( | ||||
| <ParameterSelect /> | <ParameterSelect /> | ||||
| ) : ( | ) : ( | ||||
| @@ -435,8 +475,9 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||||
| <Form.Item | <Form.Item | ||||
| key={item.key} | key={item.key} | ||||
| name={['out_parameters', item.key]} | name={['out_parameters', item.key]} | ||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'out_parameters')} | label={getLabel(item, 'out_parameters')} | ||||
| rules={[{ required: item.value.require ? true : false }]} | |||||
| rules={getFormRules(item)} | |||||
| > | > | ||||
| <ParameterInput allowClear></ParameterInput> | <ParameterInput allowClear></ParameterInput> | ||||
| </Form.Item> | </Form.Item> | ||||
| @@ -241,10 +241,10 @@ const Login: React.FC = () => { | |||||
| style={{ height: '42px', marginRight: '10px' }} | style={{ height: '42px', marginRight: '10px' }} | ||||
| alt="" | alt="" | ||||
| /> | /> | ||||
| 智能软件开发平台 | |||||
| 智能材料科研平台 | |||||
| </div> | </div> | ||||
| <div className={centerTitleBoX}> | <div className={centerTitleBoX}> | ||||
| <span style={{ whiteSpace: 'nowrap' }}>智能软件开发平台</span> | |||||
| <span style={{ whiteSpace: 'nowrap' }}>智能材料科研平台</span> | |||||
| <img | <img | ||||
| src="/assets/images/ai-logo.png" | src="/assets/images/ai-logo.png" | ||||
| @@ -271,7 +271,7 @@ const Login: React.FC = () => { | |||||
| <div className={rightTopTitle}> | <div className={rightTopTitle}> | ||||
| <span style={{ color: '#111111', fontSize: '36px' }}>hello~</span> | <span style={{ color: '#111111', fontSize: '36px' }}>hello~</span> | ||||
| <span style={{ color: '#606b7a', fontSize: '32px', marginLeft: '10px' }}>欢迎登陆</span> | <span style={{ color: '#606b7a', fontSize: '32px', marginLeft: '10px' }}>欢迎登陆</span> | ||||
| <span style={{ color: '#1664ff', fontSize: '32px' }}>智能软件开发平台</span> | |||||
| <span style={{ color: '#1664ff', fontSize: '32px' }}>智能材料科研平台</span> | |||||
| </div> | </div> | ||||
| <div className={containerLoginForm}> | <div className={containerLoginForm}> | ||||
| <div | <div | ||||
| @@ -28,9 +28,10 @@ export function deleteExperimentById(id) { | |||||
| }); | }); | ||||
| } | } | ||||
| // 根据id查询实验实例 | // 根据id查询实验实例 | ||||
| export function getQueryByExperimentId(id) { | |||||
| return request(`/api/mmp/experimentIns/queryByExperimentId/${id}`, { | |||||
| export function getQueryByExperimentId(params) { | |||||
| return request(`/api/mmp/experimentIns`, { | |||||
| method: 'GET', | method: 'GET', | ||||
| params, | |||||
| }); | }); | ||||
| } | } | ||||
| // 根据id删除实验实例 | // 根据id删除实验实例 | ||||
| @@ -4,7 +4,7 @@ | |||||
| * @Description: 定义全局类型,比如无关联的页面都需要要的类型 | * @Description: 定义全局类型,比如无关联的页面都需要要的类型 | ||||
| */ | */ | ||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||||
| // 流水线全局参数 | // 流水线全局参数 | ||||
| export type PipelineGlobalParam = { | export type PipelineGlobalParam = { | ||||
| @@ -26,9 +26,13 @@ export type ExperimentInstance = { | |||||
| status: string; | status: string; | ||||
| argo_ins_name: string; | argo_ins_name: string; | ||||
| argo_ins_ns: string; | argo_ins_ns: string; | ||||
| nodes_result: string; | |||||
| nodes_result: { | |||||
| [key: string]: any; | |||||
| }; | |||||
| nodes_status: string; | nodes_status: string; | ||||
| global_param: PipelineGlobalParam[]; | global_param: PipelineGlobalParam[]; | ||||
| tensorBoardStatus?: TensorBoardStatus; | |||||
| tensorboardUrl?: string; | |||||
| }; | }; | ||||
| // 流水线节点 | // 流水线节点 | ||||
| @@ -43,6 +47,7 @@ export type PipelineNodeModel = { | |||||
| out_parameters: string; | out_parameters: string; | ||||
| component_label: string; | component_label: string; | ||||
| icon_path: string; | icon_path: string; | ||||
| workflowId?: string; | |||||
| }; | }; | ||||
| // 流水线节点模型数据 | // 流水线节点模型数据 | ||||
| @@ -109,6 +109,29 @@ export function nullToUndefined(obj: Record<string, any>) { | |||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| // undefined to null | |||||
| export function undefinedToNull(obj: Record<string, any>) { | |||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | |||||
| for (const key in obj) { | |||||
| if (obj.hasOwnProperty(key)) { | |||||
| const value = obj[key]; | |||||
| if (value === undefined) { | |||||
| newObj[key] = null; | |||||
| } else if (Array.isArray(value)) { | |||||
| newObj[key] = value.map((item) => undefinedToNull(item)); | |||||
| } else if (isPlainObject(value)) { | |||||
| newObj[key] = undefinedToNull(value); | |||||
| } else { | |||||
| newObj[key] = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| return newObj; | |||||
| } | |||||
| /** | /** | ||||
| * Changes the property names of an object based on a mapping provided. | * Changes the property names of an object based on a mapping provided. | ||||
| * | * | ||||
| @@ -0,0 +1,66 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-06-26 16:37:39 | |||||
| * @Description: 全局网络请求 Loading | |||||
| */ | |||||
| import KFSpin from '@/components/KFSpin'; | |||||
| import { ConfigProvider, SpinProps } from 'antd'; | |||||
| import { globalConfig } from 'antd/es/config-provider'; | |||||
| import zhCN from 'antd/locale/zh_CN'; | |||||
| import { createRoot } from 'react-dom/client'; | |||||
| export class Loading { | |||||
| static total = 0; | |||||
| static show(props?: SpinProps) { | |||||
| Loading.total += 1; | |||||
| if (Loading.total > 1) { | |||||
| return; | |||||
| } | |||||
| const container = document.createElement('div'); | |||||
| container.id = 'loading'; | |||||
| const rootContainer = document.getElementsByTagName('main')[0]; | |||||
| rootContainer?.appendChild(container); | |||||
| const root = createRoot(container); | |||||
| const global = globalConfig(); | |||||
| let timeoutId: ReturnType<typeof setTimeout>; | |||||
| function render(spinProps: SpinProps) { | |||||
| clearTimeout(timeoutId); | |||||
| timeoutId = setTimeout(() => { | |||||
| const rootPrefixCls = global.getPrefixCls(); | |||||
| const iconPrefixCls = global.getIconPrefixCls(); | |||||
| const theme = global.getTheme(); | |||||
| const dom = <KFSpin {...spinProps} />; | |||||
| root.render( | |||||
| <ConfigProvider | |||||
| prefixCls={rootPrefixCls} | |||||
| iconPrefixCls={iconPrefixCls} | |||||
| theme={theme} | |||||
| locale={zhCN} | |||||
| > | |||||
| {global.holderRender ? global.holderRender(dom) : dom} | |||||
| </ConfigProvider>, | |||||
| ); | |||||
| }); | |||||
| } | |||||
| render({ size: 'large', ...props, spinning: true }); | |||||
| } | |||||
| static hide(force: boolean = false) { | |||||
| Loading.total -= 1; | |||||
| if (Loading.total <= 0 || force) { | |||||
| Loading.total = 0; | |||||
| const rootContainer = document.getElementsByTagName('main')[0]; | |||||
| const container = document.getElementById('loading'); | |||||
| if (container) { | |||||
| rootContainer?.removeChild(container); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| export default Loading; | |||||
| @@ -2,18 +2,12 @@ | |||||
| * @param { Promise } promise | * @param { Promise } promise | ||||
| * @return { Promise } | * @return { Promise } | ||||
| */ | */ | ||||
| export async function to<T>(promise: Promise<T>): Promise<[T, null] | [null, Error]> { | |||||
| export async function to<T, U = Error>(promise: Promise<T>): Promise<[T, null] | [null, U]> { | |||||
| try { | try { | ||||
| const data = await promise; | const data = await promise; | ||||
| return [data, null]; | return [data, null]; | ||||
| } catch (error) { | } catch (error) { | ||||
| if (error instanceof Error) { | |||||
| return [null, error]; | |||||
| } else if (typeof error === 'string') { | |||||
| return [null, new Error(error)]; | |||||
| } else { | |||||
| return [null, new Error('Error')]; | |||||
| } | |||||
| return [null, error as U]; | |||||
| } | } | ||||
| } | } | ||||
| @@ -59,7 +59,7 @@ public class AimServiceImpl implements AimService { | |||||
| return new ArrayList<>(); | return new ArrayList<>(); | ||||
| } | } | ||||
| //查询实例数据 | //查询实例数据 | ||||
| List<ExperimentIns> byExperimentId = experimentInsService.getByExperimentId(experimentId); | |||||
| List<ExperimentIns> byExperimentId = experimentInsService.queryByExperimentId(experimentId); | |||||
| if (byExperimentId == null || byExperimentId.size() == 0){ | if (byExperimentId == null || byExperimentId.size() == 0){ | ||||
| return new ArrayList<>(); | return new ArrayList<>(); | ||||
| @@ -111,9 +111,10 @@ public class AimServiceImpl implements AimService { | |||||
| List<String> datasetList = getTrainDateSet(records, aimrunId); | List<String> datasetList = getTrainDateSet(records, aimrunId); | ||||
| aimRunInfo.setDataset(datasetList); | aimRunInfo.setDataset(datasetList); | ||||
| } | } | ||||
| aimRunInfoList.add(aimRunInfo); | |||||
| } | } | ||||
| } | } | ||||
| aimRunInfoList.add(aimRunInfo); | |||||
| } | } | ||||
| //判断哪个最长 | //判断哪个最长 | ||||
| @@ -240,7 +240,7 @@ public class DatasetVersionServiceImpl implements DatasetVersionService { | |||||
| datasetVersion.setFileName(dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType()); | datasetVersion.setFileName(dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType()); | ||||
| datasetVersion.setFileSize(formattedSize); | datasetVersion.setFileSize(formattedSize); | ||||
| datasetVersion.setUrl(url); | |||||
| datasetVersion.setUrl(objectName); | |||||
| datasetVersion.setDescription(labelDatasetVersionVo.getDesc()); | datasetVersion.setDescription(labelDatasetVersionVo.getDesc()); | ||||
| this.insert(datasetVersion); | this.insert(datasetVersion); | ||||
| } | } | ||||