| @@ -48,12 +48,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.1.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: null, | |||
| @@ -80,12 +101,38 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.3.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 120, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [], | |||
| @@ -110,12 +157,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.31.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [], | |||
| @@ -140,12 +208,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.4.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [ | |||
| @@ -154,12 +243,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.6.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [], | |||
| @@ -231,12 +341,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.5.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [ | |||
| @@ -275,12 +406,33 @@ export default defineMock({ | |||
| exp_ins_id: null, | |||
| version: 'v0.11.0', | |||
| ref_item: null, | |||
| train_task: {}, | |||
| train_dataset: [], | |||
| train_params: [], | |||
| train_image: null, | |||
| test_dataset: [], | |||
| project_dependency: {}, | |||
| train_task: { | |||
| name: '模型训练测试导出0529', | |||
| ins_id: 229, | |||
| task_id: 'model-train-5d76f002', | |||
| }, | |||
| train_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| train_params: ['256', '2'], | |||
| train_image: | |||
| '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim', | |||
| test_dataset: [ | |||
| { | |||
| dataset_id: 20, | |||
| dataset_version: 'v0.1.0', | |||
| dataset_name: '手写体识别模型依赖测试训练数据集', | |||
| }, | |||
| ], | |||
| project_dependency: { | |||
| url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', | |||
| name: 'somun202304241505581', | |||
| branch: 'train_ci_test', | |||
| }, | |||
| parent_models_map: [], | |||
| parent_models: [], | |||
| children_models: [], | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-17 12:53:06 | |||
| * @Description: | |||
| * @Description: 封装 iconfont 组件 | |||
| */ | |||
| import '@/iconfont/iconfont-menu.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-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100%; | |||
| background-size: 100% 100%; | |||
| } | |||
| @@ -62,3 +62,7 @@ | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| .parameter-input.parameter-input--error { | |||
| border-color: @error-color; | |||
| } | |||
| @@ -1,18 +1,28 @@ | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Input } from 'antd'; | |||
| import { Form, Input } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type ParameterInputData = { | |||
| value?: any; | |||
| showValue?: any; | |||
| fromSelect?: boolean; | |||
| } & Record<string, any>; | |||
| // 对象 | |||
| export type ParameterInputObject = { | |||
| value?: any; // 值 | |||
| showValue?: any; // 显示值 | |||
| fromSelect?: boolean; // 是否来自选择 | |||
| activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab | |||
| expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | |||
| checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | |||
| [key: string]: any; | |||
| }; | |||
| interface ParameterInputProps { | |||
| value?: ParameterInputData; | |||
| onChange?: (value: ParameterInputData) => void; | |||
| // 值类型 | |||
| export type ParameterInputValue = ParameterInputObject | string; | |||
| export interface ParameterInputProps { | |||
| value?: ParameterInputValue; | |||
| onChange?: (value?: ParameterInputValue) => void; | |||
| onClick?: () => void; | |||
| onRemove?: () => void; | |||
| canInput?: boolean; | |||
| textArea?: boolean; | |||
| placeholder?: string; | |||
| @@ -21,12 +31,14 @@ interface ParameterInputProps { | |||
| style?: React.CSSProperties; | |||
| size?: 'middle' | 'small' | 'large'; | |||
| disabled?: boolean; | |||
| id?: string; | |||
| } | |||
| function ParameterInput({ | |||
| value, | |||
| onChange, | |||
| onClick, | |||
| onRemove, | |||
| canInput = true, | |||
| textArea = false, | |||
| allowClear, | |||
| @@ -34,6 +46,7 @@ function ParameterInput({ | |||
| style, | |||
| size = 'middle', | |||
| disabled = false, | |||
| id, | |||
| ...rest | |||
| }: ParameterInputProps) { | |||
| const valueObj = | |||
| @@ -42,16 +55,34 @@ function ParameterInput({ | |||
| valueObj.showValue = valueObj.value; | |||
| } | |||
| const isSelect = valueObj?.fromSelect; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| const { status } = Form.Item.useStatus(); | |||
| // 删除 | |||
| const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { | |||
| e.stopPropagation(); | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| onRemove?.(); | |||
| }; | |||
| return ( | |||
| <> | |||
| {(isSelect || !canInput) && !disabled ? ( | |||
| <div | |||
| id={id} | |||
| className={classNames( | |||
| 'parameter-input', | |||
| { 'parameter-input--large': size === 'large' }, | |||
| { [`parameter-input--${status}`]: status }, | |||
| className, | |||
| )} | |||
| style={style} | |||
| @@ -62,18 +93,7 @@ function ParameterInput({ | |||
| <span className="parameter-input__content__value">{valueObj?.showValue}</span> | |||
| <CloseOutlined | |||
| className="parameter-input__content__close-icon" | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: undefined, | |||
| checkedKeys: undefined, | |||
| }); | |||
| }} | |||
| onClick={handleRemove} | |||
| /> | |||
| </div> | |||
| ) : ( | |||
| @@ -83,6 +103,7 @@ function ParameterInput({ | |||
| ) : ( | |||
| <InputComponent | |||
| {...rest} | |||
| id={id} | |||
| size={size} | |||
| className={className} | |||
| style={style} | |||
| @@ -93,9 +114,9 @@ function ParameterInput({ | |||
| onChange={(e) => | |||
| onChange?.({ | |||
| ...valueObj, | |||
| fromSelect: false, | |||
| value: e.target.value, | |||
| showValue: e.target.value, | |||
| fromSelect: false, | |||
| }) | |||
| } | |||
| /> | |||
| @@ -105,3 +126,12 @@ function ParameterInput({ | |||
| } | |||
| export default ParameterInput; | |||
| // 必填校验 | |||
| export const requiredValidator = (rule: RuleObject, value: any) => { | |||
| const trueValue = typeof value === 'object' ? value?.value : value; | |||
| if (!trueValue) { | |||
| return Promise.reject(rule.message || '必填项'); | |||
| } | |||
| return Promise.resolve(); | |||
| }; | |||
| @@ -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) { | |||
| const [visible, setVisible] = useState(initialValue); | |||
| const ref = useRef(initialValue); | |||
| const open = useCallback(() => { | |||
| setVisible(true); | |||
| @@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) { | |||
| setVisible(false); | |||
| }, []); | |||
| return [visible, open, close] as const; | |||
| useEffect(() => { | |||
| ref.current = visible; | |||
| }, [visible]); | |||
| return [visible, open, close, ref] as const; | |||
| } | |||
| type Callback<T> = (state: T) => void; | |||
| @@ -1,35 +1,32 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 创建镜像 | |||
| * @Description: 创建开发环境 | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import ParameterInput from '@/components/ParameterInput'; | |||
| import ResourceSelect, { | |||
| requiredValidator, | |||
| type ParameterInputObject, | |||
| } from '@/components/ResourceSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorResponse, | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| import { createEditorReq } from '@/services/developmentEnvironment'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { App, Button, Col, Form, Input, Row, Select } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { useState } from 'react'; | |||
| import { omit, pick } from 'lodash'; | |||
| import styles from './index.less'; | |||
| type FormData = { | |||
| name: string; | |||
| computing_resource: string; | |||
| standard: string; | |||
| image: string; | |||
| model: ResourceSelectorResponse; | |||
| dataset: ResourceSelectorResponse; | |||
| image: ParameterInputObject; | |||
| model: ParameterInputObject; | |||
| dataset: ParameterInputObject; | |||
| }; | |||
| enum ComputingResourceType { | |||
| @@ -55,25 +52,20 @@ function EditorCreate() { | |||
| const [form] = Form.useForm(); | |||
| const { message } = App.useApp(); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | |||
| const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); // 选择的模型,为了再次打开时恢复原来的选择 | |||
| const [selectedDataset, setSelectedDataset] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); // 选择的数据集,为了再次打开时恢复原来的选择 | |||
| const [selectedMirror, setSelectedMirror] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); // 选择的镜像,为了再次打开时恢复原来的选择 | |||
| // 创建编辑器 | |||
| const createEditor = async (formData: FormData) => { | |||
| // const { model, dataset } = formData; | |||
| // const params = { | |||
| // ...formData, | |||
| // model: JSON.stringify(omit(model, ['showValue'])), | |||
| // dataset: JSON.stringify(dataset, ['showValue']), | |||
| // }; | |||
| const [res] = await to(createEditorReq(formData)); | |||
| // 根据后台要求,修改表单数据 | |||
| const image = formData['image']; | |||
| const model = formData['model']; | |||
| const dataset = formData['dataset']; | |||
| const params = { | |||
| ...omit(formData, ['image', 'model', 'dataset']), | |||
| image: image.value, | |||
| model: pick(model, ['id', 'version', 'path', 'showValue']), | |||
| dataset: pick(dataset, ['id', 'version', 'path', 'showValue']), | |||
| }; | |||
| const [res] = await to(createEditorReq(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| navgite(-1); | |||
| @@ -89,61 +81,6 @@ function EditorCreate() { | |||
| const cancel = () => { | |||
| navgite(-1); | |||
| }; | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | |||
| return <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 ( | |||
| <div className={styles['editor-create']}> | |||
| @@ -230,64 +167,46 @@ function EditorCreate() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="镜像" | |||
| label="镜 像" | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像', | |||
| validator: requiredValidator, | |||
| message: '请选择镜像', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Mirror} | |||
| placeholder="请选择镜像" | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| <Col span={10}> | |||
| <Button | |||
| size="large" | |||
| type="link" | |||
| icon={getSelectBtnIcon(ResourceSelectorType.Mirror)} | |||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||
| > | |||
| 选择镜像 | |||
| </Button> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="模型" | |||
| label="模 型" | |||
| name="model" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| validator: requiredValidator, | |||
| message: '请选择模型', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Model} | |||
| placeholder="请选择模型" | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| <Col span={10}> | |||
| <Button | |||
| size="large" | |||
| type="link" | |||
| icon={getSelectBtnIcon(ResourceSelectorType.Model)} | |||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||
| > | |||
| 选择模型 | |||
| </Button> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| @@ -296,29 +215,20 @@ function EditorCreate() { | |||
| name="dataset" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| validator: requiredValidator, | |||
| message: '请选择数据集', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('dataset', ResourceSelectorType.Dataset)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| <Col span={10}> | |||
| <Button | |||
| size="large" | |||
| type="link" | |||
| icon={getSelectBtnIcon(ResourceSelectorType.Dataset)} | |||
| onClick={() => selectResource('dataset', ResourceSelectorType.Dataset)} | |||
| > | |||
| 选择数据集 | |||
| </Button> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 开发环境 | |||
| * @Description: 开发环境列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| 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 { | |||
| border: none !important; | |||
| } | |||
| .ant-table-thead { | |||
| .ant-table-cell { | |||
| background-color: rgb(247, 247, 247); | |||
| border-color: #e8e8e8 !important; | |||
| } | |||
| } | |||
| .ant-table-tbody { | |||
| .ant-table-cell { | |||
| border-right: none !important; | |||
| @@ -7,17 +7,13 @@ import { | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, /*TablePaginationConfig,*/ TableProps } from 'antd'; | |||
| import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | |||
| import { ComparisonType, comparisonConfig } from './config'; | |||
| import styles from './index.less'; | |||
| export enum ComparisonType { | |||
| Train = 'Train', // 训练 | |||
| Evaluate = 'Evaluate', // 评估 | |||
| } | |||
| type TableData = { | |||
| experiment_ins_id: number; | |||
| run_id: string; | |||
| @@ -32,13 +28,15 @@ type TableData = { | |||
| function ExperimentComparison() { | |||
| const [searchParams] = useSearchParams(); | |||
| const comparisonType = searchParams.get('type'); | |||
| const comparisonType = searchParams.get('type') as ComparisonType; | |||
| const experimentId = searchParams.get('id'); | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| // const [cacheState, setCacheState] = useCacheState(); | |||
| // const [total, setTotal] = useState(0); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const [loading, setLoading] = useState(false); | |||
| const { message } = App.useApp(); | |||
| const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); | |||
| // const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| // cacheState?.pagination ?? { | |||
| // current: 1, | |||
| @@ -52,9 +50,11 @@ function ExperimentComparison() { | |||
| // 获取对比数据列表 | |||
| const getComparisonData = async () => { | |||
| setLoading(true); | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const [res] = await to(request(experimentId)); | |||
| setLoading(false); | |||
| if (res && res.data) { | |||
| // const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(res.data); | |||
| @@ -91,6 +91,7 @@ function ExperimentComparison() { | |||
| // 选择行 | |||
| const rowSelection: TableProps['rowSelection'] = { | |||
| type: 'checkbox', | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { | |||
| console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); | |||
| @@ -108,7 +109,9 @@ function ExperimentComparison() { | |||
| title: '实例 ID', | |||
| dataIndex: 'experiment_ins_id', | |||
| key: 'experiment_ins_id', | |||
| width: '20%', | |||
| width: 100, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| @@ -116,43 +119,61 @@ function ExperimentComparison() { | |||
| dataIndex: 'start_time', | |||
| key: 'start_time', | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(false, dateFormatter), | |||
| }, | |||
| { | |||
| title: '运行状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: '20%', | |||
| width: 100, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: ExperimentStatusCell, | |||
| }, | |||
| { | |||
| title: '训练数据集', | |||
| title: `${config.title}数据集`, | |||
| dataIndex: 'dataset', | |||
| key: 'dataset', | |||
| width: '20%', | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(true, arrayFormatter()), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| title: '训练参数', | |||
| title: `${config.title}参数`, | |||
| align: 'center', | |||
| children: first?.params_names.map((name) => ({ | |||
| title: name, | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: '20%', | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| })), | |||
| }, | |||
| { | |||
| title: '训练指标', | |||
| title: `${config.title}指标`, | |||
| align: 'center', | |||
| children: first?.metrics_names.map((name) => ({ | |||
| title: name, | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: '20%', | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| })), | |||
| @@ -177,9 +198,10 @@ function ExperimentComparison() { | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||
| pagination={false} | |||
| bordered={true} | |||
| loading={loading} | |||
| // pagination={{ | |||
| // ...pagination, | |||
| // total: total, | |||
| @@ -1,76 +1,158 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { getExperimentIns } from '@/services/experiment/index.js'; | |||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString } from '@/utils'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import G6 from '@antv/g6'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { Util } from '@antv/g6'; | |||
| import { Button } from 'antd'; | |||
| import { useEffect, useRef } from 'react'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { useNavigate, useParams } from 'react-router-dom'; | |||
| import ParamsModal from '../components/ViewParamsModal'; | |||
| import { experimentStatusInfo } from '../status'; | |||
| import styles from './index.less'; | |||
| import Props from './props'; | |||
| import ExperimentDrawer from './props'; | |||
| let graph = null; | |||
| function ExperimentText() { | |||
| const [message, setMessage, messageRef] = useStateRef({}); | |||
| const propsRef = useRef(); | |||
| const navgite = useNavigate(); | |||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | |||
| const [experimentIns, setExperimentIns] = useState(undefined); | |||
| const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); | |||
| const graphRef = useRef(); | |||
| const timerRef = useRef(); | |||
| const workflowRef = useRef(); | |||
| const locationParams = useParams(); // 新版本获取路由参数接口 | |||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | |||
| const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | |||
| useVisible(false); | |||
| const navigate = useNavigate(); | |||
| const width = 110; | |||
| const height = 36; | |||
| const getGraphData = (data) => { | |||
| if (graph) { | |||
| // 修改历史数据有蓝色边框的问题 | |||
| data.nodes.forEach((item) => { | |||
| item.style.stroke = '#fff'; | |||
| useEffect(() => { | |||
| initGraph(); | |||
| getWorkflow(); | |||
| const changeSize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| window.addEventListener('resize', changeSize); | |||
| return () => { | |||
| window.removeEventListener('resize', changeSize); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| } | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| propsDrawerOpenRef.current = propsDrawerOpen; | |||
| }, [propsDrawerOpen]); | |||
| // 获取流水线模版 | |||
| const getWorkflow = async () => { | |||
| const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||
| if (res && res.data && res.data.dag) { | |||
| try { | |||
| const dag = JSON.parse(res.data.dag); | |||
| dag.nodes.forEach((item) => { | |||
| item.in_parameters = JSON.parse(item.in_parameters); | |||
| item.out_parameters = JSON.parse(item.out_parameters); | |||
| item.control_strategy = JSON.parse(item.control_strategy); | |||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||
| }); | |||
| workflowRef.current = dag; | |||
| getExperimentInstance(true); | |||
| } catch (error) { | |||
| // JSON.parse 错误 | |||
| console.log(error); | |||
| } | |||
| } | |||
| }; | |||
| // 获取实验实例 | |||
| const getExperimentInstance = async (first) => { | |||
| const [res] = await to(getExperimentIns(locationParams.id)); | |||
| if (res && res.data && workflowRef.current) { | |||
| setExperimentIns(res.data); | |||
| const { status, nodes_status } = res.data; | |||
| const workflowData = workflowRef.current; | |||
| const experimentStatusObjs = JSON.parse(nodes_status); | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs?.[item.id] ?? {}; | |||
| const { finishedAt, startedAt, phase, id } = experimentNode; | |||
| item.experimentStartTime = startedAt; | |||
| item.experimentEndTime = finishedAt; | |||
| item.experimentStatus = phase; | |||
| item.workflowId = id; | |||
| item.img = phase ? `${item.imgName}-${phase}.png` : `${item.imgName}.png`; | |||
| }); | |||
| // 更新打开的抽屉数据 | |||
| if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { | |||
| const currentId = experimentNodeDataRef.current.id; | |||
| const node = workflowData.nodes.find((item) => item.id === currentId); | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| } | |||
| } | |||
| getGraphData(workflowData, first); | |||
| // 运行中或者等待中,每5秒获取一次实验实例 | |||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||
| timerRef.current = setTimeout(() => { | |||
| getExperimentInstance(false); | |||
| }, 5 * 1000); | |||
| } | |||
| if (first && status === ExperimentStatus.Pending) { | |||
| const node = workflowData.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| } | |||
| } else if (first && status === ExperimentStatus.Running) { | |||
| const node = | |||
| workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | |||
| workflowData.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| // 根据数据,渲染图 | |||
| const getGraphData = (data, first) => { | |||
| if (graph) { | |||
| const zoom = graph.getZoom(); | |||
| // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置 | |||
| const lastPoint = graph.getCanvasByPoint(0, 0); | |||
| graph.data(data); | |||
| graph.render(); | |||
| if (first) { | |||
| graph.fitView(); | |||
| } else { | |||
| graph.zoomTo(zoom); | |||
| // 获取重新渲染之后点(0, 0)在画布的位置 | |||
| const newPoint = graph.getCanvasByPoint(0, 0); | |||
| // 移动画布相对位移; | |||
| graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); | |||
| } | |||
| } else { | |||
| setTimeout(() => { | |||
| getGraphData(data); | |||
| }, 500); | |||
| } | |||
| }; | |||
| const getFirstWorkflow = (val) => { | |||
| getWorkflowById(val).then((pipelineRes) => { | |||
| if (graph && pipelineRes.data && pipelineRes.data.dag) { | |||
| getExperimentIns(locationParams.id).then((experimentRes) => { | |||
| if (experimentRes.code === 200) { | |||
| setMessage(experimentRes.data); | |||
| const experimentStatusObjs = JSON.parse(experimentRes.data.nodes_status); | |||
| const newNodeList = JSON.parse(pipelineRes.data.dag).nodes.map((item) => { | |||
| return { | |||
| ...item, | |||
| experimentEndTime: experimentStatusObjs?.[item.id]?.finishedAt, | |||
| experimentStartTime: experimentStatusObjs?.[item.id]?.startedAt, | |||
| experimentStatus: experimentStatusObjs?.[item.id]?.phase, | |||
| component_id: experimentStatusObjs?.[item.id]?.id, | |||
| img: experimentStatusObjs?.[item.id]?.phase | |||
| ? item.img.slice(0, item.img.length - 4) + | |||
| '-' + | |||
| experimentStatusObjs[item.id].phase + | |||
| '.png' | |||
| : item.img, | |||
| }; | |||
| }); | |||
| const newData = { ...JSON.parse(pipelineRes.data.dag), nodes: newNodeList }; | |||
| getGraphData(newData); | |||
| } | |||
| }); | |||
| } | |||
| }); | |||
| }; | |||
| useEffect(() => { | |||
| initGraph(); | |||
| getFirstWorkflow(locationParams.workflowId); | |||
| }, []); | |||
| const initGraph = () => { | |||
| G6.registerNode( | |||
| @@ -116,6 +198,54 @@ function ExperimentText() { | |||
| draggable: true, | |||
| }); | |||
| } | |||
| const hasRightImg = | |||
| cfg.experimentStatus === ExperimentStatus.Pending || | |||
| cfg.experimentStatus === ExperimentStatus.Running; | |||
| if (hasRightImg) { | |||
| const image = group.addShape('image', { | |||
| attrs: { | |||
| x: -10, | |||
| y: -10, | |||
| width: 20, | |||
| height: 20, | |||
| img: | |||
| cfg.experimentStatus === ExperimentStatus.Pending | |||
| ? require('@/assets/img/experiment-pending.png') | |||
| : require('@/assets/img/experiment-running.png'), | |||
| cursor: 'pointer', | |||
| }, | |||
| draggable: false, | |||
| capture: false, | |||
| }); | |||
| if (cfg.experimentStatus === ExperimentStatus.Running) { | |||
| image.animate( | |||
| (ratio) => { | |||
| const toMatrix = Util.transform( | |||
| [1, 0, 0, 0, 1, 0, 0, 0, 1], | |||
| [ | |||
| ['r', ratio * Math.PI * 2], | |||
| ['t', width / 2 - 14 + 10, -height / 2 - 6 + 10], | |||
| ], | |||
| ); | |||
| return { | |||
| matrix: toMatrix, | |||
| }; | |||
| }, | |||
| { | |||
| repeat: true, // 动画重复 | |||
| duration: 1000, | |||
| easing: 'easeLinear', | |||
| }, | |||
| ); | |||
| } else if (cfg.experimentStatus === ExperimentStatus.Pending) { | |||
| const toMatrix = Util.transform( | |||
| [1, 0, 0, 0, 1, 0, 0, 0, 1], | |||
| [['t', width / 2 - 14 + 10, -height / 2 - 6 + 10]], | |||
| ); | |||
| image.setMatrix(toMatrix); | |||
| } | |||
| } | |||
| const bbox = group.getBBox(); | |||
| const anchorPoints = this.getAnchorPoints(cfg); | |||
| anchorPoints.forEach((anchorPos, i) => { | |||
| @@ -139,12 +269,12 @@ function ExperimentText() { | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| const shape = group.get('children')?.[0]; | |||
| if (name === 'hover') { | |||
| if (value) { | |||
| shape.attr('stroke', themes['primaryColor']); | |||
| shape?.attr('stroke', themes['primaryColor']); | |||
| } else { | |||
| shape.attr('stroke', '#fff'); | |||
| shape?.attr('stroke', 'transparent'); | |||
| } | |||
| } | |||
| }, | |||
| @@ -181,7 +311,7 @@ function ExperimentText() { | |||
| defaultNode: { | |||
| type: 'rect-node', | |||
| size: [110, 36], | |||
| size: [width, height], | |||
| labelCfg: { | |||
| style: { | |||
| @@ -196,8 +326,14 @@ function ExperimentText() { | |||
| }, | |||
| style: { | |||
| fill: '#fff', | |||
| stroke: '#fff', | |||
| radius: 10, | |||
| stroke: 'transparent', | |||
| cursor: 'pointer', | |||
| radius: 8, | |||
| shadowColor: 'rgba(75, 84, 137, 0.4)', | |||
| shadowBlur: 6, | |||
| shadowOffsetX: 0, | |||
| shadowOffsetY: 0, | |||
| overflow: 'hidden', | |||
| lineWidth: 0.5, | |||
| }, | |||
| }, | |||
| @@ -224,9 +360,28 @@ function ExperimentText() { | |||
| }, | |||
| }, | |||
| }); | |||
| // 修改历史数据样式问题 | |||
| graph.node((node) => { | |||
| return { | |||
| style: { | |||
| stroke: 'transparent', | |||
| radius: 8, | |||
| }, | |||
| }; | |||
| }); | |||
| // 绑定事件 | |||
| bindEvents(); | |||
| }; | |||
| // 绑定事件 | |||
| const bindEvents = () => { | |||
| graph.on('node:click', (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| propsRef.current.showDrawer(e, locationParams.id, messageRef.current); | |||
| const model = e.item.getModel(); | |||
| setExperimentNodeData(model); | |||
| openPropsDrawer(); | |||
| } | |||
| }); | |||
| graph.on('node:mouseenter', (e) => { | |||
| @@ -235,22 +390,17 @@ function ExperimentText() { | |||
| graph.on('node:mouseleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| window.onresize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| }; | |||
| return ( | |||
| <div className={styles['pipeline-container']}> | |||
| <div className={styles['pipeline-container__top']}> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 启动时间:{formatDate(message.create_time)} | |||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 执行时长: | |||
| {elapsedTime(message.create_time, message.finish_time)} | |||
| {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 状态: | |||
| @@ -260,11 +410,11 @@ function ExperimentText() { | |||
| height: '8px', | |||
| borderRadius: '50%', | |||
| marginRight: '6px', | |||
| backgroundColor: experimentStatusInfo[message.status]?.color, | |||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[message.status]?.color }}> | |||
| {experimentStatusInfo[message.status]?.label} | |||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||
| </span> | |||
| </div> | |||
| <Button | |||
| @@ -275,11 +425,24 @@ function ExperimentText() { | |||
| </Button> | |||
| </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 | |||
| open={paramsModalOpen} | |||
| onCancel={closeParamsModal} | |||
| globalParam={message.global_param} | |||
| globalParam={experimentIns?.global_param} | |||
| ></ParamsModal> | |||
| </div> | |||
| ); | |||
| @@ -30,4 +30,10 @@ | |||
| background-image: url(/assets/images/pipeline-canvas-back.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| :global { | |||
| .ant-drawer-mask { | |||
| background: transparent !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -14,7 +14,12 @@ | |||
| border: 1px solid #e0eaff; | |||
| } | |||
| .ant-tabs-content-holder { | |||
| overflow-y: auto; | |||
| .ant-tabs-content { | |||
| height: 100%; | |||
| .ant-tabs-tabpane { | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,11 +1,9 @@ | |||
| import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Form, Tabs } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { forwardRef, useImperativeHandle, useMemo } from 'react'; | |||
| import ExperimentParameter from '../components/ExperimentParameter'; | |||
| import ExperimentResult from '../components/ExperimentResult'; | |||
| import LogList from '../components/LogList'; | |||
| @@ -19,154 +17,130 @@ export type ExperimentLog = { | |||
| start_time?: string; // 日志开始时间 | |||
| }; | |||
| const Props = forwardRef((_, ref) => { | |||
| const [form] = Form.useForm(); | |||
| const [experimentNodeData, setExperimentNodeData] = useState<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> | |||
| <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 { | |||
| height: 100%; | |||
| padding-top: 8px; | |||
| overflow-y: auto; | |||
| &__title { | |||
| display: flex; | |||
| @@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { Form, Input, Select, type FormProps } from 'antd'; | |||
| import { Form, Input, Select } from 'antd'; | |||
| import styles from './index.less'; | |||
| const { TextArea } = Input; | |||
| type ExperimentParameterProps = { | |||
| form: FormProps['form']; | |||
| nodeData: PipelineNodeModelSerialize; | |||
| }; | |||
| function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| const [resourceStandardList] = useComputingResource(); // 资源规模 | |||
| // 控制策略 | |||
| @@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||
| wrapperCol={{ | |||
| span: 24, | |||
| }} | |||
| form={form} | |||
| initialValues={nodeData} | |||
| style={{ | |||
| maxWidth: 600, | |||
| }} | |||
| @@ -1,5 +1,7 @@ | |||
| .experiment-result { | |||
| height: 100%; | |||
| padding: 8px; | |||
| overflow-y: auto; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| @@ -1,11 +1,15 @@ | |||
| import { getNodeResult } from '@/services/experiment/index.js'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { App, Button } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ExportModelModal from '../ExportModelModal'; | |||
| import styles from './index.less'; | |||
| type ExperimentResultProps = { | |||
| results?: ExperimentResultData[] | null; | |||
| experimentInsId?: number; // 实验实例 id | |||
| pipelineNodeId?: string; // 流水线节点 id | |||
| }; | |||
| type ExperimentResultData = { | |||
| @@ -18,8 +22,21 @@ type ExperimentResultData = { | |||
| }[]; | |||
| }; | |||
| function ExperimentResult({ results }: ExperimentResultProps) { | |||
| function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { | |||
| const { message } = App.useApp(); | |||
| const [experimentResults, setExperimentResults] = useState<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) => { | |||
| @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { | |||
| return ( | |||
| <div className={styles['experiment-result']}> | |||
| <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']}> | |||
| <span>{item.name}</span> | |||
| <Button | |||
| @@ -11,11 +11,11 @@ import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | |||
| import { Button } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| export type LogGroupProps = ExperimentLog & { | |||
| status: ExperimentStatus; // 实验状态 | |||
| status?: ExperimentStatus; // 实验状态 | |||
| }; | |||
| type Log = { | |||
| @@ -25,7 +25,7 @@ type Log = { | |||
| // 滚动到底部 | |||
| const scrollToBottom = (smooth: boolean = true) => { | |||
| const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; | |||
| const element = document.getElementById('log-list'); | |||
| if (element) { | |||
| const optons: ScrollToOptions = { | |||
| top: element.scrollHeight, | |||
| @@ -41,25 +41,36 @@ function LogGroup({ | |||
| pod_name = '', | |||
| log_content = '', | |||
| start_time, | |||
| status = ExperimentStatus.Pending, | |||
| status, | |||
| }: LogGroupProps) { | |||
| const [collapse, setCollapse] = useState(true); | |||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | |||
| const [completed, setCompleted] = useState(false); | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| let timerId: NodeJS.Timeout | undefined; | |||
| if (status === ExperimentStatus.Running) { | |||
| const timerId = setInterval(() => { | |||
| timerId = setInterval(() => { | |||
| 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(() => { | |||
| const mouseDown = () => { | |||
| @@ -131,7 +142,8 @@ function LogGroup({ | |||
| const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | |||
| 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 ( | |||
| <div className={styles['log-group']}> | |||
| {log_type === 'resource' && ( | |||
| @@ -1,5 +1,7 @@ | |||
| .log-list { | |||
| height: 100%; | |||
| padding: 8px; | |||
| overflow-y: auto; | |||
| &__empty { | |||
| padding: 15px; | |||
| @@ -1,18 +1,74 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| 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 styles from './index.less'; | |||
| 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 ( | |||
| <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> | |||
| )} | |||
| @@ -42,7 +42,7 @@ const statusConfig: Record<TensorBoardStatus, TensorBoardStatusInfo> = { | |||
| }; | |||
| type TensorBoardStatusProps = { | |||
| status: TensorBoardStatus; | |||
| status?: TensorBoardStatus; | |||
| onClick: () => void; | |||
| }; | |||
| @@ -1,31 +1,28 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { TensorBoardStatus } from '@/enums'; | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| import { | |||
| deleteExperimentById, | |||
| deleteQueryByExperimentInsId, | |||
| getExperiment, | |||
| getExperimentById, | |||
| getQueryByExperimentId, | |||
| getTensorBoardStatusReq, | |||
| postExperiment, | |||
| putExperiment, | |||
| putQueryByExperimentInsId, | |||
| runExperiments, | |||
| runTensorBoardReq, | |||
| } from '@/services/experiment/index.js'; | |||
| import { getWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| 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 { useEffect, useRef, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import { ComparisonType } from './Comparison'; | |||
| import { ComparisonType } from './Comparison/config'; | |||
| import AddExperimentModal from './components/AddExperimentModal'; | |||
| import TensorBoardStatusCell from './components/TensorBoardStatus'; | |||
| import ExperimentInstance from './components/ExperimentInstance'; | |||
| import Styles from './index.less'; | |||
| import { experimentStatusInfo } from './status'; | |||
| @@ -49,7 +46,18 @@ function Experiment() { | |||
| const [isAdd, setIsAdd] = useState(true); | |||
| const [isModalOpen, setIsModalOpen] = useState(false); | |||
| const [addFormData, setAddFormData] = useState({}); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| 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(() => { | |||
| getList(); | |||
| @@ -58,6 +66,7 @@ function Experiment() { | |||
| clearExperimentInTimers(); | |||
| }; | |||
| }, []); | |||
| // 获取实验列表 | |||
| const getList = async () => { | |||
| const params = { | |||
| @@ -76,6 +85,7 @@ function Experiment() { | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| // 获取流水线列表 | |||
| const getWorkflowList = async () => { | |||
| const [res, _] = await to(getWorkflow(queryFlow)); | |||
| @@ -83,39 +93,45 @@ function Experiment() { | |||
| 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); | |||
| // 获取 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 | |||
| const runTensorBoard = async (experimentIn) => { | |||
| const params = { | |||
| @@ -134,6 +150,7 @@ function Experiment() { | |||
| } | |||
| } | |||
| }; | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = async (experimentIn) => { | |||
| const params = { | |||
| @@ -155,20 +172,30 @@ function Experiment() { | |||
| return item; | |||
| }); | |||
| }); | |||
| const timerId = setTimeout(() => { | |||
| let timerId = timerIds.get(experimentIn.id); | |||
| if (timerId) { | |||
| clearTimeout(timerId); | |||
| timerIds.delete(experimentIn.id); | |||
| } | |||
| timerId = setTimeout(() => { | |||
| getTensorBoardStatus(experimentIn); | |||
| }, 10000); | |||
| }, 10 * 1000); | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }; | |||
| // 展开实例 | |||
| const expandChange = (e, record) => { | |||
| clearExperimentInTimers(); | |||
| setExperimentInList([]); | |||
| if (record.id === expandedRowKeys) { | |||
| setExpandedRowKeys(null); | |||
| } else { | |||
| getQueryByExperiment(record.id); | |||
| getQueryByExperiment(record.id, 0); | |||
| } | |||
| }; | |||
| // 终止实验实例获取 TensorBoard 状态的定时器 | |||
| const clearExperimentInTimers = () => { | |||
| timerIds.values().forEach((timerId) => { | |||
| @@ -176,6 +203,7 @@ function Experiment() { | |||
| }); | |||
| timerIds.clear(); | |||
| }; | |||
| // 创建实验 | |||
| const createExperiment = () => { | |||
| setIsAdd(true); | |||
| @@ -183,6 +211,7 @@ function Experiment() { | |||
| setExperimentId(null); | |||
| setIsModalOpen(true); | |||
| }; | |||
| // 编辑实验 | |||
| const editExperiment = (id) => { | |||
| getExperimentById(id).then((res) => { | |||
| @@ -198,10 +227,7 @@ function Experiment() { | |||
| const handleCancel = () => { | |||
| setIsModalOpen(false); | |||
| }; | |||
| const routeToEdit = (e, record) => { | |||
| e.stopPropagation(); | |||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||
| }; | |||
| // 创建或者编辑实验接口请求 | |||
| const handleAddExperiment = async (values) => { | |||
| 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) => { | |||
| pageOption.current = { | |||
| @@ -244,21 +261,29 @@ function Experiment() { | |||
| }; | |||
| 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(); | |||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||
| }; | |||
| // 跳转到实验实例详情 | |||
| const gotoInstanceInfo = (item, record) => { | |||
| navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | |||
| }; | |||
| // 处理 TensorBoard 操作 | |||
| const handleTensorboard = async (experimentIn) => { | |||
| if ( | |||
| 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) => { | |||
| 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 = [ | |||
| { | |||
| title: '实验名称', | |||
| @@ -304,7 +355,7 @@ function Experiment() { | |||
| title: '关联流水线名称', | |||
| dataIndex: '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%', | |||
| }, | |||
| { | |||
| @@ -413,7 +464,7 @@ function Experiment() { | |||
| ]; | |||
| return ( | |||
| <div className={Styles.experimentBox}> | |||
| <div className={Styles.pipelineTopBox}> | |||
| <div className={Styles.experimentTopBox}> | |||
| <Button type="default" onClick={createExperiment} icon={<KFIcon type="icon-xinjian2" />}> | |||
| 新建实验 | |||
| </Button> | |||
| @@ -427,130 +478,16 @@ function Experiment() { | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| expandable={{ | |||
| 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) => { | |||
| 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; | |||
| flex: 1; | |||
| align-items: center; | |||
| .startTime { | |||
| .singleLine(); | |||
| } | |||
| } | |||
| .status { | |||
| width: 200px; | |||
| justify-content: flex-end; | |||
| width: 100%; | |||
| height: 49px; | |||
| margin-bottom: 10px; | |||
| padding-right: 30px; | |||
| background-image: url(/assets/images/pipeline-back.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| } | |||
| .operation { | |||
| width: 334px; | |||
| } | |||
| } | |||
| .tableExpandBoxContent { | |||
| height: 45px; | |||
| background-color: #fff; | |||
| border: 1px solid #eaeaea; | |||
| & + & { | |||
| border-top: none; | |||
| } | |||
| } | |||
| .statusBox { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 200px; | |||
| .statusIcon { | |||
| visibility: hidden; | |||
| transition: all 0.2s; | |||
| } | |||
| } | |||
| .statusBox:hover .statusIcon { | |||
| visibility: visible; | |||
| } | |||
| .experimentBox { | |||
| height: 100%; | |||
| .experimentTable { | |||
| height: calc(100% - 60px); | |||
| :global { | |||
| @@ -288,7 +288,13 @@ function MirrorInfo() { | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ ...pagination, total, showSizeChanger: true, showQuickJumper: true }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| @@ -241,7 +241,7 @@ function MirrorList() { | |||
| <div className={styles['mirror-list__content']}> | |||
| <div className={styles['mirror-list__content__filter']}> | |||
| <Input.Search | |||
| placeholder="按数据集名称筛选" | |||
| placeholder="按镜像名称筛选" | |||
| allowClear | |||
| onSearch={onSearch} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| @@ -277,6 +277,7 @@ function MirrorList() { | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| @@ -1,9 +1,15 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-06-07 11:24:10 | |||
| * @Description: 模型演化 | |||
| */ | |||
| import { useEffectWhen } from '@/hooks'; | |||
| import { ResourceVersionData } from '@/pages/Dataset/config'; | |||
| import { getModelAtlasReq } from '@/services/dataset/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { G6GraphEvent, Graph } from '@antv/g6'; | |||
| import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; | |||
| // @ts-ignore | |||
| import { Flex, Select } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| @@ -11,7 +17,15 @@ import GraphLegend from '../GraphLegend'; | |||
| import NodeTooltips from '../NodeTooltips'; | |||
| import styles from './index.less'; | |||
| import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils'; | |||
| import { getGraphData, nodeFontSize, nodeHeight, nodeWidth, normalizeTreeData } from './utils'; | |||
| import { | |||
| NodeType, | |||
| getGraphData, | |||
| nodeFontSize, | |||
| nodeHeight, | |||
| nodeWidth, | |||
| normalizeTreeData, | |||
| traverseHierarchically, | |||
| } from './utils'; | |||
| type modeModelEvolutionProps = { | |||
| resourceId: number; | |||
| @@ -37,6 +51,8 @@ function ModelEvolution({ | |||
| const [hoverNodeData, setHoverNodeData] = useState< | |||
| ModelDepsData | ProjectDependency | TrainDataset | undefined | |||
| >(undefined); | |||
| const apiData = useRef<ModelDepsData | undefined>(undefined); // 接口返回的树形结构 | |||
| const hierarchyNodes = useRef<ModelDepsData[]>([]); // 层级迭代树形结构,得到的节点列表 | |||
| useEffect(() => { | |||
| initGraph(); | |||
| @@ -111,18 +127,7 @@ function ModelEvolution({ | |||
| }, | |||
| }, | |||
| modes: { | |||
| default: [ | |||
| 'drag-canvas', | |||
| 'zoom-canvas', | |||
| // { | |||
| // type: 'collapse-expand', | |||
| // onChange(item?: Item, collapsed?: boolean) { | |||
| // const data = item!.getModel(); | |||
| // data.collapsed = collapsed; | |||
| // return true; | |||
| // }, | |||
| // }, | |||
| ], | |||
| default: ['drag-canvas', 'zoom-canvas'], | |||
| }, | |||
| }); | |||
| @@ -161,11 +166,26 @@ function ModelEvolution({ | |||
| }); | |||
| graph.on('node:click', (e: G6GraphEvent) => { | |||
| const nodeItem = e.item; | |||
| const nodeItem = e.item as INode; | |||
| const model = nodeItem.getModel() as ModelDepsData | ProjectDependency | TrainDataset; | |||
| const { model_type } = model; | |||
| switch (model_type) { | |||
| if ( | |||
| model_type === NodeType.Project || | |||
| model_type === NodeType.TrainDataset || | |||
| model_type === NodeType.TestDataset || | |||
| !apiData.current || | |||
| !hierarchyNodes.current | |||
| ) { | |||
| return; | |||
| } | |||
| setShowNodeTooltip(false); | |||
| setEnterTooltip(false); | |||
| toggleExpended(model.id); | |||
| const graphData = getGraphData(apiData.current, hierarchyNodes.current); | |||
| graph.data(graphData); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| }); | |||
| // 鼠标滚轮缩放时,隐藏 tooltip | |||
| @@ -175,6 +195,17 @@ function ModelEvolution({ | |||
| }); | |||
| }; | |||
| // toggle 展开 | |||
| const toggleExpended = (id: string) => { | |||
| const nodes = hierarchyNodes.current; | |||
| for (const node of nodes) { | |||
| if (node.id === id) { | |||
| node.expanded = !node.expanded; | |||
| break; | |||
| } | |||
| } | |||
| }; | |||
| const handleTooltipsMouseEnter = () => { | |||
| setEnterTooltip(true); | |||
| }; | |||
| @@ -192,7 +223,9 @@ function ModelEvolution({ | |||
| const [res] = await to(getModelAtlasReq(params)); | |||
| if (res && res.data) { | |||
| const data = normalizeTreeData(res.data); | |||
| const graphData = getGraphData(data); | |||
| apiData.current = data; | |||
| hierarchyNodes.current = traverseHierarchically(data); | |||
| const graphData = getGraphData(data, hierarchyNodes.current); | |||
| graph.data(graphData); | |||
| graph.render(); | |||
| @@ -6,21 +6,22 @@ import Hierarchy from '@antv/hierarchy'; | |||
| export const nodeWidth = 90; | |||
| export const nodeHeight = 40; | |||
| export const vGap = nodeHeight + 20; | |||
| export const hGap = nodeWidth; | |||
| export const hGap = nodeHeight + 20; | |||
| export const ellipseWidth = nodeWidth; | |||
| export const labelPadding = 30; | |||
| export const nodeFontSize = 8; | |||
| export const datasetHGap = 20; | |||
| // 数据集节点 | |||
| const datasetNodes: NodeConfig[] = []; | |||
| export enum NodeType { | |||
| current = 'current', | |||
| parent = 'parent', | |||
| children = 'children', | |||
| project = 'project', | |||
| trainDataset = 'trainDataset', | |||
| testDataset = 'testDataset', | |||
| Current = 'Current', // 当前模型 | |||
| Parent = 'Parent', // 父模型 | |||
| Children = 'Children', // 子模型 | |||
| Project = 'Project', // 项目 | |||
| TrainDataset = 'TrainDataset', // 训练数据集 | |||
| TestDataset = 'TestDataset', // 测试数据集 | |||
| } | |||
| export type Rect = { | |||
| @@ -40,14 +41,14 @@ export interface TrainDataset extends NodeConfig { | |||
| dataset_id: number; | |||
| dataset_name: string; | |||
| dataset_version: string; | |||
| model_type: NodeType.testDataset | NodeType.trainDataset; | |||
| model_type: NodeType.TestDataset | NodeType.TrainDataset; | |||
| } | |||
| export interface ProjectDependency extends NodeConfig { | |||
| url: string; | |||
| name: string; | |||
| branch: string; | |||
| model_type: NodeType.project; | |||
| model_type: NodeType.Project; | |||
| } | |||
| export type ModalDetail = { | |||
| @@ -66,9 +67,9 @@ export interface ModelDepsAPIData { | |||
| version: string; | |||
| workflow_id: number; | |||
| exp_ins_id: number; | |||
| model_type: NodeType.children | NodeType.current | NodeType.parent; | |||
| model_type: NodeType.Children | NodeType.Current | NodeType.Parent; | |||
| current_model_name: string; | |||
| project_dependency: ProjectDependency; | |||
| project_dependency?: ProjectDependency; | |||
| test_dataset: TrainDataset[]; | |||
| train_dataset: TrainDataset[]; | |||
| train_task: TrainTask; | |||
| @@ -79,16 +80,22 @@ export interface ModelDepsAPIData { | |||
| export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData { | |||
| children: ModelDepsData[]; | |||
| expanded: boolean; // 是否展开 | |||
| level: number; // 层级,从 0 开始 | |||
| datasetLen: number; // 数据集数量 | |||
| } | |||
| // 规范化子数据 | |||
| export function normalizeChildren(data: ModelDepsData[]) { | |||
| if (Array.isArray(data)) { | |||
| data.forEach((item) => { | |||
| item.model_type = NodeType.children; | |||
| item.model_type = NodeType.Children; | |||
| item.expanded = false; | |||
| item.level = 0; | |||
| item.datasetLen = item.train_dataset.length + item.test_dataset.length; | |||
| item.id = `$M_${item.current_model_id}_${item.version}`; | |||
| item.label = getLabel(item); | |||
| item.style = getStyle(NodeType.children); | |||
| item.style = getStyle(NodeType.Children); | |||
| normalizeChildren(item.children); | |||
| }); | |||
| } | |||
| @@ -111,22 +118,22 @@ export function getLabel(node: ModelDepsData | ModelDepsAPIData) { | |||
| export function getStyle(model_type: NodeType) { | |||
| let fill = ''; | |||
| switch (model_type) { | |||
| case NodeType.current: | |||
| case NodeType.Current: | |||
| fill = 'l(0) 0:#72a1ff 1:#1664ff'; | |||
| break; | |||
| case NodeType.parent: | |||
| case NodeType.Parent: | |||
| fill = 'l(0) 0:#93dfd1 1:#43c9b1'; | |||
| break; | |||
| case NodeType.children: | |||
| case NodeType.Children: | |||
| fill = 'l(0) 0:#72b4ff 1:#169aff'; | |||
| break; | |||
| case NodeType.project: | |||
| case NodeType.Project: | |||
| fill = 'l(0) 0:#b3a9ff 1:#8981ff'; | |||
| break; | |||
| case NodeType.trainDataset: | |||
| case NodeType.TrainDataset: | |||
| fill = '#a5d878'; | |||
| break; | |||
| case NodeType.testDataset: | |||
| case NodeType.TestDataset: | |||
| fill = '#d8b578'; | |||
| break; | |||
| default: | |||
| @@ -145,11 +152,15 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||
| }) as ModelDepsData; | |||
| // 设置当前模型的数据 | |||
| normalizedData.model_type = NodeType.current; | |||
| normalizedData.model_type = NodeType.Current; | |||
| normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`; | |||
| normalizedData.label = getLabel(normalizedData); | |||
| normalizedData.style = getStyle(NodeType.current); | |||
| normalizedData.style = getStyle(NodeType.Current); | |||
| normalizedData.expanded = true; | |||
| normalizedData.datasetLen = | |||
| normalizedData.train_dataset.length + normalizedData.test_dataset.length; | |||
| normalizeChildren(normalizedData.children as ModelDepsData[]); | |||
| normalizedData.level = 0; | |||
| // 将 parent_models 转换成树形结构 | |||
| let parent_models = normalizedData.parent_models || []; | |||
| @@ -157,10 +168,13 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||
| const parent = parent_models[0]; | |||
| normalizedData = { | |||
| ...parent, | |||
| model_type: NodeType.parent, | |||
| expanded: false, | |||
| level: 0, | |||
| datasetLen: parent.train_dataset.length + parent.test_dataset.length, | |||
| model_type: NodeType.Parent, | |||
| id: `$M_${parent.current_model_id}_${parent.version}`, | |||
| label: getLabel(parent), | |||
| style: getStyle(NodeType.parent), | |||
| style: getStyle(NodeType.Parent), | |||
| children: [ | |||
| { | |||
| ...normalizedData, | |||
| @@ -174,13 +188,34 @@ export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { | |||
| } | |||
| // 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据 | |||
| export function getGraphData(data: ModelDepsData): GraphData { | |||
| export function getGraphData(data: ModelDepsData, hierarchyNodes: ModelDepsData[]): GraphData { | |||
| const config = { | |||
| direction: 'LR', | |||
| getHeight: () => nodeHeight, | |||
| getWidth: () => nodeWidth, | |||
| getVGap: () => vGap / 2, | |||
| getHGap: () => hGap / 2, | |||
| getVGap: (node: NodeConfig) => { | |||
| const model = node as ModelDepsData; | |||
| const { model_type, expanded, project_dependency } = model; | |||
| if (model_type === NodeType.Current || model_type === NodeType.Parent) { | |||
| return vGap / 2; | |||
| } | |||
| const selfGap = expanded && project_dependency?.url ? nodeHeight + vGap : 0; | |||
| const nextNode = getSameHierarchyNextNode(model, hierarchyNodes); | |||
| if (!nextNode) { | |||
| return vGap / 2; | |||
| } | |||
| const nextGap = nextNode.expanded === true && nextNode.datasetLen > 0 ? nodeHeight + vGap : 0; | |||
| return (selfGap + nextGap + vGap) / 2; | |||
| }, | |||
| getHGap: (node: NodeConfig) => { | |||
| const model = node as ModelDepsData; | |||
| return ( | |||
| (getHierarchyWidth(model.level, hierarchyNodes) + | |||
| getHierarchyWidth(model.level + 1, hierarchyNodes) + | |||
| hGap) / | |||
| 2 | |||
| ); | |||
| }, | |||
| }; | |||
| // 树形布局计算出坐标 | |||
| @@ -191,11 +226,11 @@ export function getGraphData(data: ModelDepsData): GraphData { | |||
| Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => { | |||
| const data = node.data as ModelDepsData; | |||
| // 当前模型显示数据集和项目 | |||
| if (data.model_type === NodeType.current) { | |||
| if (data.expanded === true) { | |||
| addDatasetDependency(data, node, nodes, edges); | |||
| addProjectDependency(data, node, nodes, edges); | |||
| } else if (data.model_type === NodeType.children) { | |||
| adjustDatasetPosition(node); | |||
| } else if (data.model_type === NodeType.Children) { | |||
| // adjustDatasetPosition(node); | |||
| } | |||
| nodes.push({ | |||
| ...data, | |||
| @@ -219,16 +254,16 @@ const addDatasetDependency = ( | |||
| nodes: NodeConfig[], | |||
| edges: EdgeConfig[], | |||
| ) => { | |||
| const { train_dataset, test_dataset } = data; | |||
| const { train_dataset, test_dataset, id } = data; | |||
| train_dataset.forEach((item) => { | |||
| item.id = `$DTrain_${item.dataset_id}_${item.dataset_version}`; | |||
| item.model_type = NodeType.trainDataset; | |||
| item.style = getStyle(NodeType.trainDataset); | |||
| item.id = `$DTrain_${id}_${item.dataset_id}_${item.dataset_version}`; | |||
| item.model_type = NodeType.TrainDataset; | |||
| item.style = getStyle(NodeType.TrainDataset); | |||
| }); | |||
| test_dataset.forEach((item) => { | |||
| item.id = `$DTest_${item.dataset_id}_${item.dataset_version}`; | |||
| item.model_type = NodeType.testDataset; | |||
| item.style = getStyle(NodeType.testDataset); | |||
| item.id = `$DTest_${id}_${item.dataset_id}_${item.dataset_version}`; | |||
| item.model_type = NodeType.TestDataset; | |||
| item.style = getStyle(NodeType.TestDataset); | |||
| }); | |||
| datasetNodes.length = 0; | |||
| @@ -243,7 +278,7 @@ const addDatasetDependency = ( | |||
| fittingString(node.dataset_version, ellipseWidth - labelPadding, nodeFontSize); | |||
| const half = len / 2 - 0.5; | |||
| node.x = currentNode.x! - (half - index) * (ellipseWidth + 20); | |||
| node.x = currentNode.x! - (half - index) * (ellipseWidth + datasetHGap); | |||
| node.y = currentNode.y! - nodeHeight - vGap; | |||
| nodes.push(node); | |||
| datasetNodes.push(node); | |||
| @@ -264,14 +299,14 @@ const addProjectDependency = ( | |||
| nodes: NodeConfig[], | |||
| edges: EdgeConfig[], | |||
| ) => { | |||
| const { project_dependency } = data; | |||
| const { project_dependency, id } = data; | |||
| if (project_dependency?.url) { | |||
| const node = { ...project_dependency }; | |||
| node.id = `$P_${node.url}_${node.branch}`; | |||
| node.model_type = NodeType.project; | |||
| node.id = `$P_${id}_${node.url}_${node.branch}`; | |||
| node.model_type = NodeType.Project; | |||
| node.type = 'rect'; | |||
| node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize); | |||
| node.style = getStyle(NodeType.project); | |||
| node.style = getStyle(NodeType.Project); | |||
| node.style.radius = nodeHeight / 2; | |||
| node.x = currentNode.x; | |||
| node.y = currentNode.y! + nodeHeight + vGap; | |||
| @@ -331,3 +366,49 @@ function adjustDatasetPosition(node: NodeConfig) { | |||
| }); | |||
| } | |||
| } | |||
| // 层级遍历树结构 | |||
| export function traverseHierarchically(data: ModelDepsData | undefined): ModelDepsData[] { | |||
| if (!data) return []; | |||
| let level = 0; | |||
| data.level = level; | |||
| const result: ModelDepsData[] = [data]; | |||
| let index = 0; | |||
| while (index < result.length) { | |||
| const item = result[index]; | |||
| if (item.children) { | |||
| item.children.forEach((child) => { | |||
| child.level = item.level + 1; | |||
| result.push(child); | |||
| }); | |||
| } | |||
| index++; | |||
| } | |||
| return result; | |||
| } | |||
| // 找到同层次的下一个节点 | |||
| export function getSameHierarchyNextNode(node: ModelDepsData, nodes: ModelDepsData[]) { | |||
| const index = nodes.findIndex((item) => item.id === node.id); | |||
| if (index >= 0 && index < nodes.length - 1) { | |||
| const nextNode = nodes[index + 1]; | |||
| if (nextNode.level === node.level) { | |||
| return nextNode; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| // 得到层级的宽度 | |||
| export function getHierarchyWidth(level: number, nodes: ModelDepsData[]) { | |||
| const hierarchyNodes = nodes | |||
| .filter((item) => item.level === level && item.expanded === true) | |||
| .sort((a, b) => b.datasetLen - a.datasetLen); | |||
| const first = hierarchyNodes[0]; | |||
| if (first) { | |||
| return Math.max(((first.datasetLen - 1) * (nodeWidth + datasetHGap)) / 2, 0); | |||
| } | |||
| return 0; | |||
| } | |||
| @@ -22,7 +22,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||
| }; | |||
| const gotoModelPage = () => { | |||
| if (data.model_type === NodeType.current) { | |||
| if (data.model_type === NodeType.Current) { | |||
| return; | |||
| } | |||
| if (data.current_model_id === resourceId) { | |||
| @@ -39,7 +39,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||
| <div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <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']}> | |||
| {data.model_version_dependcy_vo?.name || '--'} | |||
| </span> | |||
| @@ -199,14 +199,14 @@ function NodeTooltips({ | |||
| if (!data) return null; | |||
| let Component = null; | |||
| const { model_type } = data; | |||
| if (model_type === NodeType.testDataset || model_type === NodeType.trainDataset) { | |||
| if (model_type === NodeType.TestDataset || model_type === NodeType.TrainDataset) { | |||
| Component = <DatasetInfo data={data} />; | |||
| } else if (model_type === NodeType.project) { | |||
| } else if (model_type === NodeType.Project) { | |||
| Component = <ProjectInfo data={data} />; | |||
| } 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} />; | |||
| } | |||
| @@ -5,22 +5,20 @@ | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import ParameterInput from '@/components/ParameterInput'; | |||
| import ResourceSelect, { | |||
| requiredValidator, | |||
| type ParameterInputObject, | |||
| } from '@/components/ResourceSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorResponse, | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| import { | |||
| createModelDeploymentReq, | |||
| restartModelDeploymentReq, | |||
| updateModelDeploymentReq, | |||
| } from '@/services/modelDeployment'; | |||
| import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { | |||
| getSessionStorageItem, | |||
| @@ -39,13 +37,8 @@ import styles from './index.less'; | |||
| export type FormData = { | |||
| serviceName: string; // 服务名称 | |||
| description: string; // 描述 | |||
| model: { | |||
| id: number; | |||
| version: string; | |||
| value: string; | |||
| showValue: string; | |||
| }; // 模型 | |||
| image: string; // 镜像 | |||
| model: ParameterInputObject; // 模型 | |||
| image: ParameterInputObject; // 镜像 | |||
| resource: string; // 资源规格 | |||
| replicas: string; // 副本数量 | |||
| modelPath: string; // 模型路径 | |||
| @@ -56,16 +49,10 @@ function ModelDeploymentCreate() { | |||
| const navgite = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | |||
| const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); // 选择的模型,为了再次打开时恢复原来的选择 | |||
| const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create); | |||
| const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>( | |||
| undefined, | |||
| ); | |||
| const [selectedMirror, setSelectedMirror] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); // 选择的镜像,为了再次打开时恢复原来的选择 | |||
| const { message } = App.useApp(); | |||
| 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 envList = formData['env'] ?? []; | |||
| const image = formData['image']; | |||
| const model = formData['model']; | |||
| const env = envList.reduce((acc, cur) => { | |||
| acc[cur.key] = cur.value; | |||
| return acc; | |||
| }, {} as Record<string, string>); | |||
| // 根据后台要求,修改表单数据 | |||
| const object = camelCaseToUnderscore({ | |||
| ...omit(formData, ['replicas', 'env']), | |||
| ...omit(formData, ['replicas', 'env', 'image', 'model']), | |||
| replicas: Number(formData.replicas), | |||
| env, | |||
| image: image.value, | |||
| model: pick(model, ['id', 'version', 'path', 'showValue']), | |||
| }); | |||
| const params = | |||
| @@ -258,31 +203,21 @@ function ModelDeploymentCreate() { | |||
| name="model" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| validator: requiredValidator, | |||
| message: '请选择模型', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Model} | |||
| placeholder="请选择模型" | |||
| disabled={disabled} | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| <Col span={10}> | |||
| <Button | |||
| disabled={disabled} | |||
| size="large" | |||
| type="link" | |||
| icon={getSelectBtnIcon(ResourceSelectorType.Model)} | |||
| onClick={() => selectResource('model', ResourceSelectorType.Model)} | |||
| > | |||
| 选择模型 | |||
| </Button> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| @@ -291,29 +226,20 @@ function ModelDeploymentCreate() { | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像', | |||
| validator: requiredValidator, | |||
| message: '请选择镜像', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ParameterInput | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Mirror} | |||
| placeholder="请选择镜像" | |||
| canInput={false} | |||
| size="large" | |||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| <Col span={10}> | |||
| <Button | |||
| size="large" | |||
| type="link" | |||
| icon={getSelectBtnIcon(ResourceSelectorType.Mirror)} | |||
| onClick={() => selectResource('image', ResourceSelectorType.Mirror)} | |||
| > | |||
| 选择镜像 | |||
| </Button> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| @@ -223,7 +223,7 @@ function ModelDeployment() { | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 350, | |||
| width: 250, | |||
| key: 'operation', | |||
| render: (_: any, record: ModelDeploymentData) => ( | |||
| <div> | |||
| @@ -336,6 +336,7 @@ function ModelDeployment() { | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="service_id" | |||
| @@ -37,6 +37,7 @@ export type SelectorTypeInfo = { | |||
| litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 | |||
| fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 | |||
| tabItems: TabsProps['items']; // tab 列表 | |||
| buttontTitle: string; // 按钮 title | |||
| }; | |||
| // 获取镜像文件列表,为了兼容数据集和模型 | |||
| @@ -77,6 +78,7 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||
| label: '公开模型', | |||
| }, | |||
| ], | |||
| buttontTitle: '选择模型', | |||
| }, | |||
| [ResourceSelectorType.Dataset]: { | |||
| getList: getDatasetList, | |||
| @@ -98,6 +100,7 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||
| label: '公开数据集', | |||
| }, | |||
| ], | |||
| buttontTitle: '选择数据集', | |||
| }, | |||
| [ResourceSelectorType.Mirror]: { | |||
| getList: getMirrorListReq, | |||
| @@ -121,5 +124,6 @@ export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> | |||
| label: '公开镜像', | |||
| }, | |||
| ], | |||
| buttontTitle: '选择镜像', | |||
| }, | |||
| }; | |||
| @@ -39,7 +39,7 @@ export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| defaultExpandedKeys?: React.Key[]; | |||
| defaultCheckedKeys?: React.Key[]; | |||
| defaultActiveTab?: CommonTabKeys; | |||
| onOk?: (params: ResourceSelectorResponse | null) => void; | |||
| onOk?: (params: ResourceSelectorResponse | undefined) => void; | |||
| } | |||
| type TreeRef = GetRef<typeof Tree<TreeDataNode>>; | |||
| @@ -279,7 +279,7 @@ function ResourceSelectorModal({ | |||
| }; | |||
| onOk?.(res); | |||
| } else { | |||
| onOk?.(null); | |||
| onOk?.(undefined); | |||
| } | |||
| }; | |||
| @@ -86,30 +86,42 @@ const EditPipeline = () => { | |||
| // 保存 | |||
| const savePipeline = async (val) => { | |||
| const [res, error] = await to(paramsDrawerRef.current.validateFields()); | |||
| if (error) { | |||
| const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); | |||
| if (globalParamError) { | |||
| message.error('全局参数配置有误'); | |||
| openParamsDrawer(); | |||
| return; | |||
| } | |||
| closeParamsDrawer(); | |||
| const [propsRes, propsError] = await to(propsRef.current.getFieldsValue()); | |||
| const [propsRes, propsError] = await to(propsRef.current.validateFields()); | |||
| if (propsError) { | |||
| message.error('节点必填项必须配置'); | |||
| return; | |||
| } | |||
| propsRef.current.propClose(); | |||
| propsRef.current.close(); | |||
| setTimeout(() => { | |||
| const data = graph.save(); | |||
| console.log(data); | |||
| const errorNode = data.nodes.find((item) => { | |||
| return item.formError === true; | |||
| }); | |||
| if (errorNode) { | |||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | |||
| const graphNode = graph.findById(errorNode.id); | |||
| if (graphNode) { | |||
| openNodeDrawer(graphNode, true); | |||
| } | |||
| return; | |||
| } | |||
| const params = { | |||
| ...locationParams, | |||
| dag: JSON.stringify(data), | |||
| global_param: JSON.stringify(res.global_param), | |||
| global_param: JSON.stringify(globalParamRes.global_param), | |||
| }; | |||
| saveWorkflow(params).then((ret) => { | |||
| message.success('保存成功'); | |||
| closeParamsDrawer(); | |||
| setTimeout(() => { | |||
| if (val) { | |||
| navgite({ pathname: `/pipeline/template` }); | |||
| @@ -122,10 +134,6 @@ const EditPipeline = () => { | |||
| // 渲染数据 | |||
| const getGraphData = (data) => { | |||
| if (graph) { | |||
| // 修改历史数据有蓝色边框的问题 | |||
| data.nodes.forEach((item) => { | |||
| item.style.stroke = '#fff'; | |||
| }); | |||
| graph.data(data); | |||
| graph.render(); | |||
| } else { | |||
| @@ -283,6 +291,17 @@ const EditPipeline = () => { | |||
| } | |||
| }; | |||
| // 打开节点抽屉 | |||
| const openNodeDrawer = (node, validate = false) => { | |||
| // 获取所有的上游节点 | |||
| const parentNodes = findAllParentNodes(graph, node); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| // 打开节点编辑抽屉 | |||
| propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); | |||
| }; | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| const contextMenu = initMenu(); | |||
| @@ -302,7 +321,7 @@ const EditPipeline = () => { | |||
| ); | |||
| }, | |||
| afterDraw(cfg, group) { | |||
| const image = group.addShape('image', { | |||
| group.addShape('image', { | |||
| attrs: { | |||
| x: -45, | |||
| y: -10, | |||
| @@ -329,7 +348,26 @@ const EditPipeline = () => { | |||
| draggable: true, | |||
| }); | |||
| } | |||
| if (cfg.formError) { | |||
| group.addShape('image', { | |||
| attrs: { | |||
| x: 43, | |||
| y: -24, | |||
| width: 18, | |||
| height: 18, | |||
| img: require('@/assets/img/pipeline-warning.png'), | |||
| cursor: 'pointer', | |||
| }, | |||
| draggable: false, | |||
| capture: false, | |||
| }); | |||
| } | |||
| const bbox = group.getBBox(); | |||
| if (cfg.formError) { | |||
| bbox.y += 6; | |||
| bbox.width -= 6; | |||
| bbox.height -= 6; | |||
| } | |||
| const anchorPoints = this.getAnchorPoints(cfg); | |||
| anchorPoints.forEach((anchorPos, i) => { | |||
| group.addShape('circle', { | |||
| @@ -349,18 +387,10 @@ const EditPipeline = () => { | |||
| draggable: true, | |||
| }); | |||
| }); | |||
| return image; | |||
| }, | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| // const anchorPoints = item | |||
| // .getContainer() | |||
| // .findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| // anchorPoints.forEach((point) => { | |||
| // if (value || point.get('links') > 0) point.show(); | |||
| // else point.hide(); | |||
| // }); | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point'); | |||
| @@ -371,7 +401,7 @@ const EditPipeline = () => { | |||
| point.show(); | |||
| }); | |||
| } else { | |||
| shape.attr('stroke', '#fff'); | |||
| shape.attr('stroke', 'transparent'); | |||
| anchorPoints.forEach((point) => { | |||
| point.hide(); | |||
| }); | |||
| @@ -467,9 +497,13 @@ const EditPipeline = () => { | |||
| }, | |||
| style: { | |||
| fill: '#fff', | |||
| stroke: '#fff', | |||
| stroke: 'transparent', | |||
| cursor: 'pointer', | |||
| radius: 10, | |||
| radius: 8, | |||
| shadowColor: 'rgba(75, 84, 137, 0.4)', | |||
| shadowBlur: 6, | |||
| shadowOffsetX: 0, | |||
| shadowOffsetY: 0, | |||
| overflow: 'hidden', | |||
| lineWidth: 0.5, | |||
| }, | |||
| @@ -500,19 +534,25 @@ const EditPipeline = () => { | |||
| }, | |||
| }); | |||
| // 修改历史数据样式问题 | |||
| graph.node((node) => { | |||
| return { | |||
| style: { | |||
| stroke: 'transparent', | |||
| radius: 8, | |||
| }, | |||
| }; | |||
| }); | |||
| // 绑定事件 | |||
| bindEvents(); | |||
| }; | |||
| // 绑定事件 | |||
| const bindEvents = () => { | |||
| graph.on('node:click', (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| // 获取所有的上游节点 | |||
| const parentNodes = findAllParentNodes(graph, e.item); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| // 打开节点编辑抽屉 | |||
| propsRef.current.showDrawer(e, globalParams, parentNodes); | |||
| openNodeDrawer(e.item); | |||
| } | |||
| }); | |||
| graph.on('aftercreateedge', (e) => { | |||
| @@ -599,36 +639,32 @@ const EditPipeline = () => { | |||
| // 上下文菜单 | |||
| const initMenu = () => { | |||
| const contextMenu = new G6.Menu({ | |||
| className: 'pipeline-context-menu', | |||
| getContent(evt) { | |||
| const type = evt.item.getType(); | |||
| const cloneDisplay = type === 'node' ? 'block' : 'none'; | |||
| const cloneDisplay = type === 'node' ? 'flex' : 'none'; | |||
| return ` | |||
| <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) => { | |||
| switch (target.getAttribute('code')) { | |||
| case 'delete': | |||
| graph.removeItem(item); | |||
| break; | |||
| case 'clone': | |||
| cloneElement(item); | |||
| break; | |||
| default: | |||
| break; | |||
| const id = target.id; | |||
| if (id.startsWith('clone')) { | |||
| cloneElement(item); | |||
| } else if (id.startsWith('delete')) { | |||
| graph.removeItem(item); | |||
| } | |||
| }, | |||
| // offsetX and offsetY include the padding of the parent container | |||
| @@ -685,7 +721,7 @@ const EditPipeline = () => { | |||
| </div> | |||
| <div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div> | |||
| </div> | |||
| <Props ref={propsRef} onParentChange={handleFormChange}></Props> | |||
| <Props ref={propsRef} onFormChange={handleFormChange}></Props> | |||
| <GlobalParamsDrawer | |||
| ref={paramsDrawerRef} | |||
| open={paramsDrawerOpen} | |||
| @@ -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 ParameterInput from '@/components/ParameterInput'; | |||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { | |||
| PipelineGlobalParam, | |||
| PipelineNodeModel, | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| @@ -25,10 +26,10 @@ import { canInput, createMenuItems } from './utils'; | |||
| const { TextArea } = Input; | |||
| type PipelineNodeParameterProps = { | |||
| onParentChange: (data: PipelineNodeModelSerialize) => void; | |||
| onFormChange: (data: PipelineNodeModelSerialize) => void; | |||
| }; | |||
| const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParameterProps, ref) => { | |||
| const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => { | |||
| const [form] = Form.useForm(); | |||
| const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | |||
| {} as PipelineNodeModelSerialize, | |||
| @@ -37,19 +38,27 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | |||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| const afterOpenChange = () => { | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| console.log('getFieldsValue', form.getFieldsValue()); | |||
| const control_strategy = form.getFieldValue('control_strategy'); | |||
| const in_parameters = form.getFieldValue('in_parameters'); | |||
| const out_parameters = form.getFieldValue('out_parameters'); | |||
| onParentChange({ | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| const fields = form.getFieldsValue(); | |||
| const control_strategy = JSON.stringify(fields.control_strategy); | |||
| const in_parameters = JSON.stringify(fields.in_parameters); | |||
| const out_parameters = JSON.stringify(fields.out_parameters); | |||
| console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...form.getFieldsValue(), | |||
| control_strategy: JSON.stringify(control_strategy), | |||
| in_parameters: JSON.stringify(in_parameters), | |||
| out_parameters: JSON.stringify(out_parameters), | |||
| }); | |||
| ...fields, | |||
| control_strategy: control_strategy, | |||
| in_parameters: in_parameters, | |||
| out_parameters: out_parameters, | |||
| formError: !!error, | |||
| }; | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| const onClose = () => { | |||
| @@ -57,45 +66,53 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| }; | |||
| useImperativeHandle(ref, () => ({ | |||
| getFieldsValue: async () => { | |||
| const [propsRes, propsError] = await to(form.validateFields()); | |||
| if (propsRes && !propsError) { | |||
| const values = form.getFieldsValue(); | |||
| return values; | |||
| } else { | |||
| return Promise.reject(propsError); | |||
| } | |||
| }, | |||
| showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) { | |||
| if (e.item && e.item.getModel()) { | |||
| showDrawer( | |||
| model: PipelineNodeModel, | |||
| params: PipelineGlobalParam[], | |||
| parentNodes: INode[], | |||
| validate: boolean = false, | |||
| ) { | |||
| try { | |||
| const nodeData: PipelineNodeModelSerialize = { | |||
| ...model, | |||
| in_parameters: JSON.parse(model.in_parameters), | |||
| out_parameters: JSON.parse(model.out_parameters), | |||
| control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| console.log('model', nodeData); | |||
| setStagingItem({ | |||
| ...nodeData, | |||
| }); | |||
| form.resetFields(); | |||
| const model = e.item.getModel(); | |||
| try { | |||
| const nodeData = { | |||
| ...model, | |||
| in_parameters: JSON.parse(model.in_parameters), | |||
| out_parameters: JSON.parse(model.out_parameters), | |||
| control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| console.log('model', nodeData); | |||
| setStagingItem({ | |||
| ...nodeData, | |||
| }); | |||
| form.setFieldsValue({ | |||
| ...nodeData, | |||
| }); | |||
| } catch (error) { | |||
| console.log(error); | |||
| form.setFieldsValue({ | |||
| ...nodeData, | |||
| }); | |||
| if (validate) { | |||
| form.validateFields(); | |||
| } | |||
| setOpen(true); | |||
| // 参数下拉菜单 | |||
| setMenuItems(createMenuItems(params, parentNodes)); | |||
| } catch (error) { | |||
| console.log(error); | |||
| } | |||
| setOpen(true); | |||
| // 参数下拉菜单 | |||
| setMenuItems(createMenuItems(params, parentNodes)); | |||
| }, | |||
| propClose: () => { | |||
| close: () => { | |||
| onClose(); | |||
| }, | |||
| validateFields: async () => { | |||
| if (!open) { | |||
| return; | |||
| } | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| return values; | |||
| } else { | |||
| form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); | |||
| return Promise.reject(error); | |||
| } | |||
| }, | |||
| })); | |||
| // 选择数据集、模型、镜像 | |||
| @@ -115,7 +132,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| type = ResourceSelectorType.Mirror; | |||
| break; | |||
| } | |||
| const fieldValue = form.getFieldValue(formItemName); | |||
| const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | |||
| const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | |||
| @@ -162,8 +178,21 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| }); | |||
| } | |||
| } else { | |||
| form.setFieldValue(formItemName, ''); | |||
| if (type === ResourceSelectorType.Mirror && formItemName === 'image') { | |||
| form.setFieldValue(formItemName, undefined); | |||
| } else { | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| } | |||
| } | |||
| form.validateFields([formItemName]); | |||
| close(); | |||
| }, | |||
| }); | |||
| @@ -212,6 +241,18 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| ); | |||
| }; | |||
| // 必填项校验规则 | |||
| const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => { | |||
| return item.value.require | |||
| ? [ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '必填项', | |||
| }, | |||
| ] | |||
| : []; | |||
| }; | |||
| // 控制策略 | |||
| const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| @@ -232,7 +273,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| <Drawer | |||
| title="编辑任务" | |||
| placement="right" | |||
| rootStyle={{ marginTop: '45px' }} | |||
| rootStyle={{ marginTop: '52px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| @@ -255,6 +296,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| maxWidth: 600, | |||
| }} | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| > | |||
| <div className={styles['pipeline-drawer__title']}> | |||
| <SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle> | |||
| @@ -351,7 +393,6 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| value: 'standard', | |||
| }} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| @@ -382,11 +423,14 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| > | |||
| <TextArea placeholder="请输入环境变量" allowClear /> | |||
| </Form.Item> | |||
| {/* 控制参数 */} | |||
| {controlStrategyList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['control_strategy', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'control_strategy')} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| @@ -401,11 +445,7 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| required={item.value.require ? true : false} | |||
| > | |||
| <div className={styles['pipeline-drawer__ref-row']}> | |||
| <Form.Item | |||
| name={['in_parameters', item.key]} | |||
| noStyle | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||
| {item.value.type === 'select' ? ( | |||
| <ParameterSelect /> | |||
| ) : ( | |||
| @@ -435,8 +475,9 @@ const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParame | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'out_parameters')} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| @@ -28,9 +28,10 @@ export function deleteExperimentById(id) { | |||
| }); | |||
| } | |||
| // 根据id查询实验实例 | |||
| export function getQueryByExperimentId(id) { | |||
| return request(`/api/mmp/experimentIns/queryByExperimentId/${id}`, { | |||
| export function getQueryByExperimentId(params) { | |||
| return request(`/api/mmp/experimentIns`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 根据id删除实验实例 | |||
| @@ -4,7 +4,7 @@ | |||
| * @Description: 定义全局类型,比如无关联的页面都需要要的类型 | |||
| */ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| // 流水线全局参数 | |||
| export type PipelineGlobalParam = { | |||
| @@ -26,9 +26,13 @@ export type ExperimentInstance = { | |||
| status: string; | |||
| argo_ins_name: string; | |||
| argo_ins_ns: string; | |||
| nodes_result: string; | |||
| nodes_result: { | |||
| [key: string]: any; | |||
| }; | |||
| nodes_status: string; | |||
| global_param: PipelineGlobalParam[]; | |||
| tensorBoardStatus?: TensorBoardStatus; | |||
| tensorboardUrl?: string; | |||
| }; | |||
| // 流水线节点 | |||
| @@ -43,6 +47,7 @@ export type PipelineNodeModel = { | |||
| out_parameters: string; | |||
| component_label: string; | |||
| icon_path: string; | |||
| workflowId?: string; | |||
| }; | |||
| // 流水线节点模型数据 | |||
| @@ -109,6 +109,29 @@ export function nullToUndefined(obj: Record<string, any>) { | |||
| return newObj; | |||
| } | |||
| // undefined to null | |||
| export function undefinedToNull(obj: Record<string, any>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| const value = obj[key]; | |||
| if (value === undefined) { | |||
| newObj[key] = null; | |||
| } else if (Array.isArray(value)) { | |||
| newObj[key] = value.map((item) => undefinedToNull(item)); | |||
| } else if (isPlainObject(value)) { | |||
| newObj[key] = undefinedToNull(value); | |||
| } else { | |||
| newObj[key] = value; | |||
| } | |||
| } | |||
| } | |||
| return newObj; | |||
| } | |||
| /** | |||
| * Changes the property names of an object based on a mapping provided. | |||
| * | |||
| @@ -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 | |||
| * @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 { | |||
| const data = await promise; | |||
| return [data, null]; | |||
| } catch (error) { | |||
| if (error instanceof Error) { | |||
| return [null, error]; | |||
| } else if (typeof error === 'string') { | |||
| return [null, new Error(error)]; | |||
| } else { | |||
| return [null, new Error('Error')]; | |||
| } | |||
| return [null, error as U]; | |||
| } | |||
| } | |||
| @@ -240,7 +240,7 @@ public class DatasetVersionServiceImpl implements DatasetVersionService { | |||
| datasetVersion.setFileName(dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType()); | |||
| datasetVersion.setFileSize(formattedSize); | |||
| datasetVersion.setUrl(url); | |||
| datasetVersion.setUrl(objectName); | |||
| datasetVersion.setDescription(labelDatasetVersionVo.getDesc()); | |||
| this.insert(datasetVersion); | |||
| } | |||