Browse Source

Merge remote-tracking branch 'origin/dev' into dev

pull/102/head
西大锐 1 year ago
parent
commit
fafa46b587
60 changed files with 1926 additions and 1032 deletions
  1. +1
    -1
      react-ui/config/config.ts
  2. +1
    -1
      react-ui/config/defaultSettings.ts
  3. +1
    -1
      react-ui/config/routes.ts
  4. +194
    -42
      react-ui/mock/model.ts
  5. BIN
      react-ui/public/assets/images/left-top-logo-1.png
  6. BIN
      react-ui/public/assets/images/left-top-logo.png
  7. BIN
      react-ui/src/assets/img/experiment-pending.png
  8. BIN
      react-ui/src/assets/img/experiment-running.png
  9. BIN
      react-ui/src/assets/img/pipeline-warning.png
  10. +1
    -1
      react-ui/src/components/KFIcon/index.tsx
  11. +19
    -0
      react-ui/src/components/KFSpin/index.less
  12. +13
    -0
      react-ui/src/components/KFSpin/index.tsx
  13. +1
    -1
      react-ui/src/components/PageTitle/index.less
  14. +4
    -0
      react-ui/src/components/ParameterInput/index.less
  15. +53
    -23
      react-ui/src/components/ParameterInput/index.tsx
  16. +11
    -0
      react-ui/src/components/ResourceSelect/index.less
  17. +104
    -0
      react-ui/src/components/ResourceSelect/index.tsx
  18. +6
    -1
      react-ui/src/hooks/index.ts
  19. +1
    -1
      react-ui/src/iconfont/iconfont.js
  20. +36
    -126
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  21. +1
    -1
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  22. +17
    -0
      react-ui/src/pages/Experiment/Comparison/config.tsx
  23. +6
    -0
      react-ui/src/pages/Experiment/Comparison/index.less
  24. +40
    -18
      react-ui/src/pages/Experiment/Comparison/index.tsx
  25. +230
    -67
      react-ui/src/pages/Experiment/Info/index.jsx
  26. +6
    -0
      react-ui/src/pages/Experiment/Info/index.less
  27. +6
    -1
      react-ui/src/pages/Experiment/Info/props.less
  28. +124
    -150
      react-ui/src/pages/Experiment/Info/props.tsx
  29. +69
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
  30. +178
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  31. +2
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  32. +3
    -4
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  33. +2
    -0
      react-ui/src/pages/Experiment/components/ExperimentResult/index.less
  34. +22
    -5
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  35. +23
    -11
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  36. +2
    -0
      react-ui/src/pages/Experiment/components/LogList/index.less
  37. +62
    -6
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  38. +1
    -1
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx
  39. +125
    -188
      react-ui/src/pages/Experiment/index.jsx
  40. +12
    -86
      react-ui/src/pages/Experiment/index.less
  41. +7
    -1
      react-ui/src/pages/Mirror/Info/index.tsx
  42. +2
    -1
      react-ui/src/pages/Mirror/List/index.tsx
  43. +50
    -17
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  44. +122
    -41
      react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
  45. +7
    -7
      react-ui/src/pages/Model/components/NodeTooltips/index.tsx
  46. +22
    -96
      react-ui/src/pages/ModelDeployment/Create/index.tsx
  47. +2
    -1
      react-ui/src/pages/ModelDeployment/List/index.tsx
  48. +4
    -0
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
  49. +2
    -2
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx
  50. +91
    -55
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  51. +34
    -0
      react-ui/src/pages/Pipeline/editPipeline/index.less
  52. +98
    -57
      react-ui/src/pages/Pipeline/editPipeline/props.tsx
  53. +3
    -3
      react-ui/src/pages/User/Login/index.tsx
  54. +3
    -2
      react-ui/src/services/experiment/index.js
  55. +7
    -2
      react-ui/src/types.ts
  56. +23
    -0
      react-ui/src/utils/index.ts
  57. +66
    -0
      react-ui/src/utils/loading.tsx
  58. +2
    -8
      react-ui/src/utils/promise.ts
  59. +3
    -2
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AimServiceImpl.java
  60. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java

+ 1
- 1
react-ui/config/config.ts View File

@@ -76,7 +76,7 @@ export default defineConfig({
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
title: '智能软件开发平台',
title: '智能材料科研平台',
layout: {
locale: false,
...defaultSettings,


+ 1
- 1
react-ui/config/defaultSettings.ts View File

@@ -16,7 +16,7 @@ const Settings: ProLayoutProps & {
fixSiderbar: false,
splitMenus: false,
colorWeak: false,
title: '智能软件开发平台',
title: '智能材料科研平台',
pwa: true,
logo: '/assets/images/left-top-logo.png',
token: {


+ 1
- 1
react-ui/config/routes.ts View File

@@ -112,7 +112,7 @@ export default [
{
name: '开发环境',
path: '',
component: './DevelopmentEnvironment/List',
component: './DevelopmentEnvironment/Editor',
},
{
name: '创建编辑器',


+ 194
- 42
react-ui/mock/model.ts View File

@@ -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: [],


BIN
react-ui/public/assets/images/left-top-logo-1.png View File

Before After
Width: 79  |  Height: 86  |  Size: 4.8 kB

BIN
react-ui/public/assets/images/left-top-logo.png View File

Before After
Width: 79  |  Height: 86  |  Size: 4.8 kB Width: 112  |  Height: 112  |  Size: 5.3 kB

BIN
react-ui/src/assets/img/experiment-pending.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.6 kB

BIN
react-ui/src/assets/img/experiment-running.png View File

Before After
Width: 60  |  Height: 60  |  Size: 2.1 kB

BIN
react-ui/src/assets/img/pipeline-warning.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.7 kB

+ 1
- 1
react-ui/src/components/KFIcon/index.tsx View File

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-17 12:53:06
* @Description:
* @Description: 封装 iconfont 组件
*/
import '@/iconfont/iconfont-menu.js';
import '@/iconfont/iconfont.js';


+ 19
- 0
react-ui/src/components/KFSpin/index.less View File

@@ -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;
}
}

+ 13
- 0
react-ui/src/components/KFSpin/index.tsx View File

@@ -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;

+ 1
- 1
react-ui/src/components/PageTitle/index.less View File

@@ -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%;
}

+ 4
- 0
react-ui/src/components/ParameterInput/index.less View File

@@ -62,3 +62,7 @@
font-size: 12px;
}
}

.parameter-input.parameter-input--error {
border-color: @error-color;
}

+ 53
- 23
react-ui/src/components/ParameterInput/index.tsx View File

@@ -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();
};

+ 11
- 0
react-ui/src/components/ResourceSelect/index.less View File

@@ -0,0 +1,11 @@
.kf-resource-select {
position: relative;
display: flex;
align-items: center;

&__button {
position: absolute;
top: 0;
left: calc(100% + 10px);
}
}

+ 104
- 0
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -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;

+ 6
- 1
react-ui/src/hooks/index.ts View File

@@ -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
- 1
react-ui/src/iconfont/iconfont.js
File diff suppressed because it is too large
View File


+ 36
- 126
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -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
- 1
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 开发环境
* @Description: 开发环境列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';


+ 17
- 0
react-ui/src/pages/Experiment/Comparison/config.tsx View File

@@ -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: '评估',
},
};

+ 6
- 0
react-ui/src/pages/Experiment/Comparison/index.less View File

@@ -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;


+ 40
- 18
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -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,


+ 230
- 67
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -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>
);


+ 6
- 0
react-ui/src/pages/Experiment/Info/index.less View File

@@ -30,4 +30,10 @@
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}

:global {
.ant-drawer-mask {
background: transparent !important;
}
}
}

+ 6
- 1
react-ui/src/pages/Experiment/Info/props.less View File

@@ -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%;
}
}
}
}
}


+ 124
- 150
react-ui/src/pages/Experiment/Info/props.tsx View File

@@ -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;

+ 69
- 0
react-ui/src/pages/Experiment/components/ExperimentInstance/index.less View File

@@ -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;
}

+ 178
- 0
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -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;

+ 2
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.less View File

@@ -1,5 +1,7 @@
.experiment-parameter {
height: 100%;
padding-top: 8px;
overflow-y: auto;

&__title {
display: flex;


+ 3
- 4
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -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,
}}


+ 2
- 0
react-ui/src/pages/Experiment/components/ExperimentResult/index.less View File

@@ -1,5 +1,7 @@
.experiment-result {
height: 100%;
padding: 8px;
overflow-y: auto;
color: @text-color;
font-size: 14px;



+ 22
- 5
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -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


+ 23
- 11
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -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' && (


+ 2
- 0
react-ui/src/pages/Experiment/components/LogList/index.less View File

@@ -1,5 +1,7 @@
.log-list {
height: 100%;
padding: 8px;
overflow-y: auto;

&__empty {
padding: 15px;


+ 62
- 6
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -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>
)}


+ 1
- 1
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx View File

@@ -42,7 +42,7 @@ const statusConfig: Record<TensorBoardStatus, TensorBoardStatusInfo> = {
};

type TensorBoardStatusProps = {
status: TensorBoardStatus;
status?: TensorBoardStatus;
onClick: () => void;
};



+ 125
- 188
react-ui/src/pages/Experiment/index.jsx View File

@@ -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);
},


+ 12
- 86
react-ui/src/pages/Experiment/index.less View File

@@ -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 {


+ 7
- 1
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -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"
/>


+ 2
- 1
react-ui/src/pages/Mirror/List/index.tsx View File

@@ -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"


+ 50
- 17
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -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();


+ 122
- 41
react-ui/src/pages/Model/components/ModelEvolution/utils.tsx View File

@@ -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;
}

+ 7
- 7
react-ui/src/pages/Model/components/NodeTooltips/index.tsx View File

@@ -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} />;
}


+ 22
- 96
react-ui/src/pages/ModelDeployment/Create/index.tsx View File

@@ -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}>


+ 2
- 1
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -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"


+ 4
- 0
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx View File

@@ -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: '选择镜像',
},
};

+ 2
- 2
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx View File

@@ -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);
}
};



+ 91
- 55
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -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}


+ 34
- 0
react-ui/src/pages/Pipeline/editPipeline/index.less View File

@@ -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;
}
}
}
}

+ 98
- 57
react-ui/src/pages/Pipeline/editPipeline/props.tsx View File

@@ -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>


+ 3
- 3
react-ui/src/pages/User/Login/index.tsx View File

@@ -241,10 +241,10 @@ const Login: React.FC = () => {
style={{ height: '42px', marginRight: '10px' }}
alt=""
/>
智能软件开发平台
智能材料科研平台
</div>
<div className={centerTitleBoX}>
<span style={{ whiteSpace: 'nowrap' }}>智能软件开发平台</span>
<span style={{ whiteSpace: 'nowrap' }}>智能材料科研平台</span>

<img
src="/assets/images/ai-logo.png"
@@ -271,7 +271,7 @@ const Login: React.FC = () => {
<div className={rightTopTitle}>
<span style={{ color: '#111111', fontSize: '36px' }}>hello~</span>
<span style={{ color: '#606b7a', fontSize: '32px', marginLeft: '10px' }}>欢迎登陆</span>
<span style={{ color: '#1664ff', fontSize: '32px' }}>智能软件开发平台</span>
<span style={{ color: '#1664ff', fontSize: '32px' }}>智能材料科研平台</span>
</div>
<div className={containerLoginForm}>
<div


+ 3
- 2
react-ui/src/services/experiment/index.js View File

@@ -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删除实验实例


+ 7
- 2
react-ui/src/types.ts View File

@@ -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;
};

// 流水线节点模型数据


+ 23
- 0
react-ui/src/utils/index.ts View File

@@ -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.
*


+ 66
- 0
react-ui/src/utils/loading.tsx View File

@@ -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
- 8
react-ui/src/utils/promise.ts View File

@@ -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];
}
}



+ 3
- 2
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AimServiceImpl.java View File

@@ -59,7 +59,7 @@ public class AimServiceImpl implements AimService {
return new ArrayList<>();
}
//查询实例数据
List<ExperimentIns> byExperimentId = experimentInsService.getByExperimentId(experimentId);
List<ExperimentIns> byExperimentId = experimentInsService.queryByExperimentId(experimentId);

if (byExperimentId == null || byExperimentId.size() == 0){
return new ArrayList<>();
@@ -111,9 +111,10 @@ public class AimServiceImpl implements AimService {
List<String> datasetList = getTrainDateSet(records, aimrunId);
aimRunInfo.setDataset(datasetList);
}
aimRunInfoList.add(aimRunInfo);
}
}
aimRunInfoList.add(aimRunInfo);
}
//判断哪个最长



+ 1
- 1
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java View File

@@ -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);
}


Loading…
Cancel
Save