Browse Source

Merge pull request '合并dev' (#56) from dev into master

pull/71/head
cp3hnu 1 year ago
parent
commit
299cf582c5
100 changed files with 4068 additions and 1498 deletions
  1. +1
    -0
      react-ui/package.json
  2. +3
    -2
      react-ui/src/app.tsx
  3. +10
    -1
      react-ui/src/components/KFModal/index.tsx
  4. +5
    -4
      react-ui/src/components/ParameterInput/index.tsx
  5. +72
    -0
      react-ui/src/components/ParameterSelect/config.tsx
  6. +58
    -0
      react-ui/src/components/ParameterSelect/index.tsx
  7. +26
    -1
      react-ui/src/hooks/index.ts
  8. +2
    -3
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  9. +2
    -3
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  10. +3
    -4
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  11. +3
    -3
      react-ui/src/pages/Dataset/components/CategoryItem/index.tsx
  12. +1
    -1
      react-ui/src/pages/Dataset/components/CategoryList/index.tsx
  13. +65
    -0
      react-ui/src/pages/Dataset/components/ResourceIntro/index.less
  14. +153
    -0
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  15. +3
    -6
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  16. +1
    -1
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  17. +4
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.less
  18. +226
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  19. +1
    -1
      react-ui/src/pages/Dataset/components/Resourcetem/index.tsx
  20. +55
    -8
      react-ui/src/pages/Dataset/config.tsx
  21. +1
    -1
      react-ui/src/pages/Dataset/index.tsx
  22. +0
    -263
      react-ui/src/pages/Dataset/intro.jsx
  23. +0
    -82
      react-ui/src/pages/Dataset/intro.less
  24. +8
    -0
      react-ui/src/pages/Dataset/intro.tsx
  25. +1
    -1
      react-ui/src/pages/DatasetPreparation/DatasetAnnotation/index.tsx
  26. +10
    -17
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  27. +5
    -0
      react-ui/src/pages/Experiment/components/ExperimentResult/index.less
  28. +30
    -26
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  29. +5
    -0
      react-ui/src/pages/Experiment/components/LogGroup/index.less
  30. +57
    -3
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  31. +10
    -0
      react-ui/src/pages/Experiment/components/LogList/index.less
  32. +5
    -3
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  33. +3
    -3
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less
  34. +4
    -4
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
  35. +9
    -8
      react-ui/src/pages/Experiment/index.jsx
  36. +8
    -3
      react-ui/src/pages/Experiment/index.less
  37. +10
    -8
      react-ui/src/pages/Experiment/status.ts
  38. +43
    -81
      react-ui/src/pages/Experiment/training/index.jsx
  39. +2
    -1
      react-ui/src/pages/Experiment/training/props.tsx
  40. +8
    -15
      react-ui/src/pages/Mirror/Info/index.tsx
  41. +1
    -1
      react-ui/src/pages/Mirror/List/index.tsx
  42. +17
    -0
      react-ui/src/pages/Model/components/GraphLegand/index.less
  43. +55
    -0
      react-ui/src/pages/Model/components/GraphLegand/index.tsx
  44. +18
    -0
      react-ui/src/pages/Model/components/ModelEvolution/index.less
  45. +521
    -0
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  46. +56
    -0
      react-ui/src/pages/Model/components/NodeTooltips/index.less
  47. +78
    -0
      react-ui/src/pages/Model/components/NodeTooltips/index.tsx
  48. +1
    -1
      react-ui/src/pages/Model/index.tsx
  49. +0
    -262
      react-ui/src/pages/Model/intro.jsx
  50. +0
    -80
      react-ui/src/pages/Model/intro.less
  51. +8
    -0
      react-ui/src/pages/Model/intro.tsx
  52. +1
    -1
      react-ui/src/pages/ModelDeployment/Create/index.tsx
  53. +17
    -28
      react-ui/src/pages/ModelDeployment/Info/index.less
  54. +145
    -121
      react-ui/src/pages/ModelDeployment/Info/index.tsx
  55. +7
    -5
      react-ui/src/pages/Pipeline/components/ModelMenu/index.less
  56. +91
    -0
      react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx
  57. +4
    -3
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
  58. +32
    -36
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx
  59. +79
    -146
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  60. +5
    -4
      react-ui/src/pages/Pipeline/editPipeline/index.less
  61. +0
    -67
      react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx
  62. +104
    -107
      react-ui/src/pages/Pipeline/editPipeline/props.tsx
  63. +1
    -0
      react-ui/src/pages/Pipeline/editPipeline/utils.tsx
  64. +1
    -2
      react-ui/src/pages/Pipeline/index.jsx
  65. +1
    -1
      react-ui/src/pages/Pipeline/index.less
  66. +2
    -2
      react-ui/src/pages/Workspace/components/QuickStart/index.tsx
  67. +1
    -0
      react-ui/src/requestConfig.ts
  68. +8
    -0
      react-ui/src/services/dataset/index.js
  69. +8
    -0
      react-ui/src/services/modelDeployment/index.ts
  70. +2
    -2
      react-ui/src/styles/menu.less
  71. +4
    -1
      react-ui/src/styles/theme.less
  72. +16
    -6
      react-ui/src/types.ts
  73. +1
    -1
      react-ui/src/utils/downloadfile.ts
  74. +93
    -6
      react-ui/src/utils/index.ts
  75. +11
    -7
      react-ui/src/utils/ui.tsx
  76. +90
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java
  77. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java
  78. +32
    -3
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java
  79. +115
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelDependencyController.java
  80. +2
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelsVersionController.java
  81. +213
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java
  82. +247
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ModelDependency.java
  83. +14
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/dependencydomain/ProjectDepency.java
  84. +17
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/dependencydomain/TrainTaskDepency.java
  85. +84
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java
  86. +88
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ModelDependencyDao.java
  87. +50
    -9
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java
  88. +57
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java
  89. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ExperimentInsService.java
  90. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ExperimentService.java
  91. +6
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/JupyterService.java
  92. +65
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ModelDependencyService.java
  93. +2
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ModelsVersionService.java
  94. +2
    -9
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetServiceImpl.java
  95. +125
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java
  96. +3
    -3
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java
  97. +233
    -22
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java
  98. +65
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java
  99. +252
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelDependencyServiceImpl.java
  100. +1
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelsServiceImpl.java

+ 1
- 0
react-ui/package.json View File

@@ -57,6 +57,7 @@
"@ant-design/pro-components": "^2.4.4", "@ant-design/pro-components": "^2.4.4",
"@ant-design/use-emotion-css": "1.0.4", "@ant-design/use-emotion-css": "1.0.4",
"@antv/g6": "^4.8.24", "@antv/g6": "^4.8.24",
"@antv/hierarchy": "^0.6.12",
"@umijs/route-utils": "^4.0.1", "@umijs/route-utils": "^4.0.1",
"antd": "^5.4.4", "antd": "^5.4.4",
"classnames": "^2.3.2", "classnames": "^2.3.2",


+ 3
- 2
react-ui/src/app.tsx View File

@@ -21,6 +21,7 @@ import './styles/menu.less';
export { requestConfig as request } from './requestConfig'; export { requestConfig as request } from './requestConfig';
// const isDev = process.env.NODE_ENV === 'development'; // const isDev = process.env.NODE_ENV === 'development';
import { menuItemRender } from '@/utils/menuRender'; import { menuItemRender } from '@/utils/menuRender';
import { gotoLoginPage } from './utils/ui';
/** /**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state * @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */ * */
@@ -45,7 +46,7 @@ export async function getInitialState(): Promise<{
} as API.CurrentUser; } as API.CurrentUser;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
history.push(PageEnum.LOGIN);
gotoLoginPage();
} }
return undefined; return undefined;
}; };
@@ -97,7 +98,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
const { location } = history; const { location } = history;
// 如果没有登录,重定向到 login // 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) { if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) {
history.push(PageEnum.LOGIN);
gotoLoginPage();
} }
}, },
layoutBgImgList: [ layoutBgImgList: [


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

@@ -12,12 +12,21 @@ import './index.less';
export interface KFModalProps extends ModalProps { export interface KFModalProps extends ModalProps {
image?: string; image?: string;
} }
function KFModal({ title, image, children, className = '', centered, ...rest }: KFModalProps) {
function KFModal({
title,
image,
children,
className = '',
centered,
maskClosable,
...rest
}: KFModalProps) {
return ( return (
<Modal <Modal
className={classNames(['kf-modal', className])} className={classNames(['kf-modal', className])}
{...rest} {...rest}
centered={centered ?? true} centered={centered ?? true}
maskClosable={maskClosable ?? false}
title={<ModalTitle title={title} image={image}></ModalTitle>} title={<ModalTitle title={title} image={image}></ModalTitle>}
> >
{children} {children}


+ 5
- 4
react-ui/src/components/ParameterInput/index.tsx View File

@@ -29,7 +29,6 @@ function ParameterInput({
onClick, onClick,
canInput = true, canInput = true,
textArea = false, textArea = false,
placeholder,
allowClear, allowClear,
className, className,
style, style,
@@ -37,8 +36,6 @@ function ParameterInput({
disabled = false, disabled = false,
...rest ...rest
}: ParameterInputProps) { }: ParameterInputProps) {
// console.log('ParameterInput', value);

const valueObj = const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) { if (valueObj && !valueObj.showValue) {
@@ -46,6 +43,7 @@ function ParameterInput({
} }
const isSelect = valueObj?.fromSelect; const isSelect = valueObj?.fromSelect;
const InputComponent = textArea ? Input.TextArea : Input; const InputComponent = textArea ? Input.TextArea : Input;
const placeholder = valueObj?.placeholder;


return ( return (
<> <>
@@ -68,9 +66,12 @@ function ParameterInput({
e.stopPropagation(); e.stopPropagation();
onChange?.({ onChange?.({
...valueObj, ...valueObj,
fromSelect: false,
value: undefined, value: undefined,
showValue: undefined, showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: undefined,
checkedKeys: undefined,
}); });
}} }}
/> />


+ 72
- 0
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -0,0 +1,72 @@
import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { type SelectProps } from 'antd';

// 过滤资源规格
const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
input: string,
option?: ComputingResource,
) => {
return (
option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false
);
};

// id 从 number 转换为 string
const convertId = (item: any) => ({ ...item, id: String(item.id) });

export type SelectPropsConfig = {
getOptions: () => Promise<any>;
fieldNames?: SelectProps['fieldNames'];
optionFilterProp?: SelectProps['optionFilterProp'];
filterOption?: SelectProps['filterOption'];
};

export const paramSelectConfig: Record<string, SelectPropsConfig> = {
dataset: {
getOptions: async () => {
const res = await getDatasetList({
page: 0,
size: 1000,
available_range: 0,
});
return res?.data?.content?.map(convertId) ?? [];
},
fieldNames: {
label: 'name',
value: 'id',
},
optionFilterProp: 'name',
},
model: {
getOptions: async () => {
const res = await getModelList({
page: 0,
size: 1000,
available_range: 0,
});
return res?.data?.content?.map(convertId) ?? [];
},
fieldNames: {
label: 'name',
value: 'id',
},
optionFilterProp: 'name',
},
resource: {
getOptions: async () => {
const res = await getComputingResourceReq({
page: 0,
size: 1000,
resource_type: '',
});
return res?.data?.content ?? [];
},
fieldNames: {
label: 'description',
value: 'standard',
},
filterOption: filterResourceStandard as SelectProps['filterOption'],
},
};

+ 58
- 0
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -0,0 +1,58 @@
import { PipelineNodeModelParameter } from '@/types';
import { to } from '@/utils/promise';
import { Select } from 'antd';
import { useEffect, useState } from 'react';
import { paramSelectConfig } from './config';

type ParameterSelectProps = {
value?: PipelineNodeModelParameter;
onChange?: (value: PipelineNodeModelParameter) => void;
disabled?: boolean;
};

function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) {
const [options, setOptions] = useState([]);
const valueNonNullable = value ?? ({} as PipelineNodeModelParameter);
const { item_type } = valueNonNullable;
const propsConfig = paramSelectConfig[item_type];

useEffect(() => {
getSelectOptions();
}, []);

const hangleChange = (e: string) => {
onChange?.({
...valueNonNullable,
value: e,
});
};

// 获取下拉数据
const getSelectOptions = async () => {
if (!propsConfig) {
return;
}
const getOptions = propsConfig.getOptions;
const [res] = await to(getOptions());
if (res) {
setOptions(res);
}
};

return (
<Select
placeholder={valueNonNullable.placeholder}
filterOption={propsConfig?.filterOption}
options={options}
fieldNames={propsConfig?.fieldNames}
value={valueNonNullable.value}
optionFilterProp={propsConfig.optionFilterProp}
onChange={hangleChange}
disabled={disabled}
showSearch
allowClear
/>
);
}

export default ParameterSelect;

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

@@ -1,7 +1,7 @@
/* /*
* @Author: 赵伟 * @Author: 赵伟
* @Date: 2024-04-15 10:01:29 * @Date: 2024-04-15 10:01:29
* @Description:
* @Description: 自定义 hooks
*/ */
import { FormInstance } from 'antd'; import { FormInstance } from 'antd';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
@@ -126,3 +126,28 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => {
} }
}, [form, prevOpen, open]); }, [form, prevOpen, open]);
}; };

/**
* Executes the effect function when the specified condition is true.
*
* @param effect - The effect function to execute.
* @param deps - The dependencies for the effect.
* @param when - The condition to trigger the effect.
*/
export const useEffectWhen = (effect: () => void, deps: React.DependencyList, when: boolean) => {
const requestFns = useRef<(() => void)[]>([]);
useEffect(() => {
if (when) {
effect();
} else {
requestFns.current.splice(0, 1, effect);
}
}, deps);

useEffect(() => {
if (when) {
const fn = requestFns.current.pop();
fn?.();
}
}, [when]);
};

+ 2
- 3
react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx View File

@@ -7,7 +7,6 @@ import { getDictSelectOption } from '@/services/system/dict';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { import {
App,
Button, Button,
Form, Form,
Input, Input,
@@ -15,12 +14,13 @@ import {
Select, Select,
Upload, Upload,
UploadFile, UploadFile,
message,
type ModalProps, type ModalProps,
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CategoryData } from '../../types';
import { CategoryData } from '../../config';
import styles from './index.less'; import styles from './index.less';


interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
@@ -32,7 +32,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]); const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]);
const { message } = App.useApp();


useEffect(() => { useEffect(() => {
getClusterOptions(); getClusterOptions();


+ 2
- 3
react-ui/src/pages/Dataset/components/AddModelModal/index.tsx View File

@@ -1,18 +1,18 @@
import { getAccessToken } from '@/access'; import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData } from '@/pages/Dataset/types';
import { CategoryData } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js'; import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { import {
App,
Button, Button,
Form, Form,
Input, Input,
Select, Select,
Upload, Upload,
UploadFile, UploadFile,
message,
type ModalProps, type ModalProps,
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
@@ -28,7 +28,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {


function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) { function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const { message } = App.useApp();


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {


+ 3
- 4
react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx View File

@@ -1,16 +1,16 @@
import { getAccessToken } from '@/access'; import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { ResourceType, resourceConfig } from '@/pages/Dataset/types';
import { ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { import {
App,
Button, Button,
Form, Form,
Input, Input,
Upload, Upload,
UploadFile, UploadFile,
message,
type ModalProps, type ModalProps,
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
@@ -33,7 +33,6 @@ function AddVersionModal({
...rest ...rest
}: AddVersionModalProps) { }: AddVersionModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const { message } = App.useApp();


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
@@ -46,7 +45,7 @@ function AddVersionModal({


// 上传请求 // 上传请求
const createDatasetVersion = async (params: any) => { const createDatasetVersion = async (params: any) => {
const request = resourceConfig[resourceType].addVersionReq;
const request = resourceConfig[resourceType].addVersion;
const [res] = await to(request(params)); const [res] = await to(request(params));
if (res) { if (res) {
message.success('创建成功'); message.success('创建成功');


+ 3
- 3
react-ui/src/pages/Dataset/components/CategoryItem/index.tsx View File

@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import { CategoryData, ResourceType, resourceConfig } from '../../config';
import styles from './index.less'; import styles from './index.less';


type CategoryItemProps = { type CategoryItemProps = {
@@ -20,13 +20,13 @@ function CategoryItem({ resourceType, item, isSelected, onClick }: CategoryItemP
<img <img
className={styles['category-item__icon']} className={styles['category-item__icon']}
style={{ width: '22px' }} style={{ width: '22px' }}
src={`/assets/images/${resourceConfig[resourceType].iconPathPrefix}/${item.path}.png`}
src={`/assets/images/${resourceConfig[resourceType].prefix}/${item.path}.png`}
alt="" alt=""
/> />
<img <img
className={styles['category-item__active-icon']} className={styles['category-item__active-icon']}
style={{ width: '22px' }} style={{ width: '22px' }}
src={`/assets/images/${resourceConfig[resourceType].iconPathPrefix}/${item.path}-hover.png`}
src={`/assets/images/${resourceConfig[resourceType].prefix}/${item.path}-hover.png`}
alt="" alt=""
/> />
<span className={styles['category-item__name']}>{item.name}</span> <span className={styles['category-item__name']}>{item.name}</span>


+ 1
- 1
react-ui/src/pages/Dataset/components/CategoryList/index.tsx View File

@@ -1,5 +1,5 @@
import { Flex, Input } from 'antd'; import { Flex, Input } from 'antd';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import { CategoryData, ResourceType, resourceConfig } from '../../config';
import CategoryItem from '../CategoryItem'; import CategoryItem from '../CategoryItem';
import styles from './index.less'; import styles from './index.less';




+ 65
- 0
react-ui/src/pages/Dataset/components/ResourceIntro/index.less View File

@@ -0,0 +1,65 @@
.resource-intro {
height: 100%;

&__top {
width: 100%;
height: 110px;
margin-bottom: 10px;
padding: 20px 30px 0;
background-image: url(/assets/images/dataset-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

&__name {
margin-bottom: 12px;
color: @text-color;
font-size: 20px;
}

&__tag {
margin-right: 10px;
padding: 4px 10px;
color: @primary-color;
font-size: 14px;
background: rgba(22, 100, 255, 0.1);
border-radius: 4px;
}
}

&__bottom {
height: calc(100% - 120px);
padding: 8px 30px 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

:global {
.ant-tabs {
height: 100%;
.ant-tabs-content-holder {
height: 100%;
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
overflow-y: auto;
}
}
}
}
}
}

&__title {
margin: 30px 0 10px;
color: @text-color;
font-weight: 500;
font-size: @font-size;
}

&__intro {
color: @text-color-secondary;
font-size: 14px;
}
}

+ 153
- 0
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -0,0 +1,153 @@
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max';
import { Flex, Tabs } from 'antd';
import { useEffect, useState } from 'react';
import { ResourceData, ResourceType, resourceConfig } from '../../config';
import ResourceVersion from '../ResourceVersion';
import styles from './index.less';

type ResourceIntroProps = {
resourceType: ResourceType;
};

enum TabKeys {
Introduction = '1',
Version = '2',
Evolution = '3',
}

const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const [info, setInfo] = useState<ResourceData>({} as ResourceData);
const locationParams = useParams();
const [searchParams] = useSearchParams();
const defaultTab = searchParams.get('tab') || '1';
let versionParam = searchParams.get('version');
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const [activeTab, setActiveTab] = useState<string>(defaultTab);
const resourceId = Number(locationParams.id);
const typeName = resourceConfig[resourceType].name; // 数据集/模型

useEffect(() => {
getModelByDetail();
getVersionList();
}, []);

// 获取详情
const getModelByDetail = async () => {
const request = resourceConfig[resourceType].getInfo;
const [res] = await to(request(resourceId));
if (res) {
setInfo(res.data);
}
};

// 获取版本列表
const getVersionList = async () => {
const request = resourceConfig[resourceType].getVersions;
const [res] = await to(request(resourceId));
if (res && res.data && res.data.length > 0) {
setVersionList(
res.data.map((item: string) => {
return {
label: item,
value: item,
};
}),
);
if (versionParam) {
setVersion(versionParam);
versionParam = null;
} else {
setVersion(res.data[0]);
}
} else {
setVersion(undefined);
}
};

// 版本变化
const handleVersionChange = (value: string) => {
setVersion(value);
};

const items = [
{
key: TabKeys.Introduction,
label: `${typeName}简介`,
children: (
<>
<div className={styles['resource-intro__title']}>简介</div>
<div className={styles['resource-intro__intro']}>{info.description}</div>
</>
),
},
{
key: TabKeys.Version,
label: `${typeName}文件/版本`,
children: (
<ResourceVersion
resourceType={resourceType}
resourceId={resourceId}
resourceName={info.name}
isPublic={info.available_range === 1}
versionList={versionList}
version={version}
isActive={activeTab === TabKeys.Version}
getVersionList={getVersionList}
onVersionChange={handleVersionChange}
></ResourceVersion>
),
},
];

if (resourceType === ResourceType.Model) {
items.push({
key: TabKeys.Evolution,
label: `模型演化`,
children: (
<ModelEvolution
resourceId={resourceId}
resourceName={info.name}
versionList={versionList}
version={version}
isActive={activeTab === TabKeys.Evolution}
onVersionChange={handleVersionChange}
></ModelEvolution>
),
});
}

const infoTypePropertyName = resourceConfig[resourceType]
.infoTypePropertyName as keyof ResourceData;
const infoTagPropertyName = resourceConfig[resourceType]
.infoTagPropertyName as keyof ResourceData;

return (
<div className={styles['resource-intro']}>
<div className={styles['resource-intro__top']}>
<div className={styles['resource-intro__top__name']}>{info.name}</div>
<Flex align="center">
<div className={styles['resource-intro__top__tag']}>
{typeName} id:{info.id}
</div>
{info[infoTypePropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTypePropertyName] || '--'}
</div>
)}
{info[infoTagPropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTagPropertyName] || '--'}
</div>
)}
</Flex>
</div>
<div className={styles['resource-intro__bottom']}>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
</div>
</div>
);
};
export default ResourceIntro;

+ 3
- 6
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -7,7 +7,7 @@ import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max'; import { useNavigate } from '@umijs/max';
import { App, Button, Input, Pagination, PaginationProps } from 'antd'; import { App, Button, Input, Pagination, PaginationProps } from 'antd';
import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../types';
import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config';
import AddDatasetModal from '../AddDatasetModal'; import AddDatasetModal from '../AddDatasetModal';
import ResourceItem from '../Resourcetem'; import ResourceItem from '../Resourcetem';
import styles from './index.less'; import styles from './index.less';
@@ -129,11 +129,8 @@ function ResourceList(
activeType: dataType, activeType: dataType,
activeTag: dataTag, activeTag: dataTag,
}); });
if (resourceType === ResourceType.Dataset) {
navigate(`/dataset/dataset/${record.id}?isPublic=${isPublic}`);
} else {
navigate(`/dataset/model/${record.id}?isPublic=${isPublic}`);
}
const prefix = resourceConfig[resourceType].prefix;
navigate(`/dataset/${prefix}/${record.id}`);
}; };


// 分页切换 // 分页切换


+ 1
- 1
react-ui/src/pages/Dataset/components/ResourcePage/index.tsx View File

@@ -4,7 +4,7 @@ import { getAssetIcon } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Flex, Tabs, type TabsProps } from 'antd'; import { Flex, Tabs, type TabsProps } from 'antd';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import { CategoryData, ResourceType, resourceConfig } from '../../config';
import CategoryList from '../CategoryList'; import CategoryList from '../CategoryList';
import ResourceList, { ResourceListRef } from '../ResourceList'; import ResourceList, { ResourceListRef } from '../ResourceList';
import styles from './index.less'; import styles from './index.less';


+ 4
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.less View File

@@ -0,0 +1,4 @@
.resource-version {
color: @text-color;
font-size: @font-size-content;
}

+ 226
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -0,0 +1,226 @@
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import { useEffectWhen } from '@/hooks';
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
import {
ResourceFileData,
ResourceType,
ResourceVersionData,
resourceConfig,
} from '@/pages/Dataset/config';
import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { App, Button, Flex, Select, Table } from 'antd';
import { useState } from 'react';
import styles from './index.less';

type ResourceVersionProps = {
resourceType: ResourceType;
resourceId: number;
resourceName: string;
isPublic: boolean;
versionList: ResourceVersionData[];
version?: string;
isActive: boolean;
getVersionList: () => void;
onVersionChange: (version: string) => void;
};
function ResourceVersion({
resourceType,
resourceId,
resourceName,
isPublic,
versionList,
version,
isActive,
getVersionList,
onVersionChange,
}: ResourceVersionProps) {
const [fileList, setFileList] = useState<ResourceFileData[]>([]);
const { message } = App.useApp();

// 获取版本文件列表
useEffectWhen(
() => {
if (version) {
getFileList(version);
} else {
setFileList([]);
}
},
[resourceId, version],
isActive,
);

// 获取版本下的文件列表
const getFileList = async (version: string) => {
const params = {
version,
[resourceConfig[resourceType].fileReqParamKey]: resourceId,
};
const request = resourceConfig[resourceType].getFiles;
const [res] = await to(request(params));
if (res) {
setFileList(res?.data?.content ?? []);
}
};

// 删除版本
const deleteVersion = async () => {
const request = resourceConfig[resourceType].deleteVersion;
const params = {
[resourceConfig[resourceType].idParamKey]: resourceId,
version,
};
const [res] = await to(request(params));
if (res) {
getVersionList();
message.success('删除成功');
}
};

// 新建版本
const showModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType,
resourceId: resourceId,
initialName: resourceName,
onOk: () => {
getVersionList();
close();
},
});
};

// 处理删除
const hanldeDelete = () => {
modalConfirm({
title: '删除后,该版本将不可恢复',
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',

onOk: () => {
deleteVersion();
},
});
};

// 全部导出
const handleExport = async () => {
const url = resourceConfig[resourceType].downloadAllAction;
downLoadZip(url, { models_id: resourceId, version });
};

// 单个导出
const downloadAlone = (record: ResourceFileData) => {
const url = resourceConfig[resourceType].downloadSingleAction;
downLoadZip(`${url}/${record.id}`);
};

const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render(text: string, record: ResourceFileData, index: number) {
return <span>{index + 1}</span>;
},
},
{
title: '文件名称',
dataIndex: 'file_name',
key: 'file_name',
render: (text: string, record: ResourceFileData) => (
<a onClick={() => downloadAlone(record)}>{text}</a>
),
},
{
title: '版本号',
dataIndex: 'version',
key: 'version',
render: CommonTableCell(),
},
{
title: '文件大小',
dataIndex: 'file_size',
key: 'file_size',
render: CommonTableCell(),
},
{
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
render: DateTableCell,
},
{
title: '操作',
dataIndex: 'option',
width: '100px',
key: 'option',
render: (_: any, record: ResourceFileData) => [
<Button
type="link"
size="small"
key="download"
icon={<KFIcon type="icon-xiazai" />}
onClick={() => downloadAlone(record)}
>
下载
</Button>,
],
},
];

return (
<div className={styles['resource-version']}>
<Flex justify="space-between" align="center" style={{ margin: '30px 0' }}>
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={onVersionChange}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
</Flex>
<Flex align="center">
{!isPublic && (
<Button
type="default"
style={{ marginRight: '20px' }}
onClick={hanldeDelete}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
<Button
type="default"
disabled={!version}
onClick={handleExport}
icon={<KFIcon type="icon-xiazai" />}
>
下载
</Button>
</Flex>
</Flex>
<div style={{ marginBottom: '30px', fontSize: '15px' }}>
{fileList.length > 0 && fileList[0].description
? '版本描述:' + fileList[0].description
: null}
</div>
<Table columns={columns} dataSource={fileList} pagination={false} rowKey="id" />
</div>
);
}

export default ResourceVersion;

+ 1
- 1
react-ui/src/pages/Dataset/components/Resourcetem/index.tsx View File

@@ -3,7 +3,7 @@ import creatByImg from '@/assets/img/creatBy.png';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { Button, Flex, Typography } from 'antd'; import { Button, Flex, Typography } from 'antd';
import { ResourceData } from '../../types';
import { ResourceData } from '../../config';
import styles from './index.less'; import styles from './index.less';


type ResourceItemProps = { type ResourceItemProps = {


react-ui/src/pages/Dataset/types.tsx → react-ui/src/pages/Dataset/config.tsx View File

@@ -4,10 +4,14 @@ import {
addDatasetVersionDetail, addDatasetVersionDetail,
addModelsVersionDetail, addModelsVersionDetail,
deleteDataset, deleteDataset,
deleteDatasetVersion,
deleteModel, deleteModel,
deleteModelVersion,
getDatasetById,
getDatasetList, getDatasetList,
getDatasetVersionIdList, getDatasetVersionIdList,
getDatasetVersionsById, getDatasetVersionsById,
getModelById,
getModelList, getModelList,
getModelVersionIdList, getModelVersionIdList,
getModelVersionsById, getModelVersionsById,
@@ -24,6 +28,9 @@ type ResourceTypeInfo = {
getVersions: (params: any) => Promise<any>; getVersions: (params: any) => Promise<any>;
getFiles: (params: any) => Promise<any>; getFiles: (params: any) => Promise<any>;
deleteRecord: (params: any) => Promise<any>; deleteRecord: (params: any) => Promise<any>;
addVersion: (params: any) => Promise<any>;
deleteVersion: (params: any) => Promise<any>;
getInfo: (params: any) => Promise<any>;
name: string; name: string;
typeParamKey: string; typeParamKey: string;
tagParamKey: string; tagParamKey: string;
@@ -33,13 +40,16 @@ type ResourceTypeInfo = {
tagTitle: string; tagTitle: string;
typeValue: number; // 从 getAssetIcon 接口获取特定值的数据为 type 分类 (category_id === typeValue) typeValue: number; // 从 getAssetIcon 接口获取特定值的数据为 type 分类 (category_id === typeValue)
tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue) tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue)
iconPathPrefix: string; // 图标路径前缀
prefix: string; // 前缀
deleteModalTitle: string; // 删除弹框的title deleteModalTitle: string; // 删除弹框的title
addBtnTitle: string; // 新增按钮的title addBtnTitle: string; // 新增按钮的title
addVersionReq: (params: any) => Promise<any>;
idParamKey: string;
idParamKey: 'models_id' | 'dataset_id';
uploadAction: string; uploadAction: string;
uploadAccept?: string; uploadAccept?: string;
downloadAllAction: string;
downloadSingleAction: string;
infoTypePropertyName: string;
infoTagPropertyName: string;
}; };


export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
@@ -48,6 +58,9 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getDatasetVersionsById, getVersions: getDatasetVersionsById,
getFiles: getDatasetVersionIdList, getFiles: getDatasetVersionIdList,
deleteRecord: deleteDataset, deleteRecord: deleteDataset,
addVersion: addDatasetVersionDetail,
deleteVersion: deleteDatasetVersion,
getInfo: getDatasetById,
name: '数据集', name: '数据集',
typeParamKey: 'data_type', typeParamKey: 'data_type',
tagParamKey: 'data_tag', tagParamKey: 'data_tag',
@@ -68,19 +81,25 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
tagTitle: '研究方向/应用领域', tagTitle: '研究方向/应用领域',
typeValue: 1, typeValue: 1,
tagValue: 2, tagValue: 2,
iconPathPrefix: 'dataset',
prefix: 'dataset',
deleteModalTitle: '确定删除该条数据集实例吗?', deleteModalTitle: '确定删除该条数据集实例吗?',
addBtnTitle: '新建数据集', addBtnTitle: '新建数据集',
addVersionReq: addDatasetVersionDetail,
idParamKey: 'dataset_id', idParamKey: 'dataset_id',
uploadAction: '/api/mmp/dataset/upload', uploadAction: '/api/mmp/dataset/upload',
uploadAccept: '.zip,.tgz', uploadAccept: '.zip,.tgz',
downloadAllAction: '/api/mmp/dataset/downloadAllFilesl',
downloadSingleAction: '/api/mmp/dataset/download',
infoTypePropertyName: 'dataset_type_name',
infoTagPropertyName: 'dataset_tag_name',
}, },
[ResourceType.Model]: { [ResourceType.Model]: {
getList: getModelList, getList: getModelList,
getVersions: getModelVersionsById, getVersions: getModelVersionsById,
getFiles: getModelVersionIdList, getFiles: getModelVersionIdList,
deleteRecord: deleteModel, deleteRecord: deleteModel,
addVersion: addModelsVersionDetail,
deleteVersion: deleteModelVersion,
getInfo: getModelById,
name: '模型', name: '模型',
typeParamKey: 'model_type', typeParamKey: 'model_type',
tagParamKey: 'model_tag', tagParamKey: 'model_tag',
@@ -101,13 +120,16 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
tagTitle: '模型能力', tagTitle: '模型能力',
typeValue: 3, typeValue: 3,
tagValue: 4, tagValue: 4,
iconPathPrefix: 'model',
prefix: 'model',
deleteModalTitle: '确定删除该条模型实例吗?', deleteModalTitle: '确定删除该条模型实例吗?',
addBtnTitle: '新建模型', addBtnTitle: '新建模型',
addVersionReq: addModelsVersionDetail,
idParamKey: 'models_id', idParamKey: 'models_id',
uploadAction: '/api/mmp/models/upload', uploadAction: '/api/mmp/models/upload',
uploadAccept: undefined, uploadAccept: undefined,
downloadAllAction: '/api/mmp/models/downloadAllFiles',
downloadSingleAction: '/api/mmp/models/download_model',
infoTypePropertyName: 'model_type_name',
infoTagPropertyName: 'model_tag_name',
}, },
}; };


@@ -119,11 +141,36 @@ export type CategoryData = {
path: string; path: string;
}; };


// 数据类型
// 资源数据
export type ResourceData = { export type ResourceData = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
create_by: string; create_by: string;
update_time: string; update_time: string;
available_range: number;
model_type_name?: string;
model_tag_name?: string;
dataset_type_name?: string;
dataset_tag_name?: string;
};

// 版本数据
export type ResourceVersionData = {
label: string;
value: string;
};

// 版本文件数据
export type ResourceFileData = {
id: number;
file_name: string;
file_size: string;
description: string;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
url: string;
version: string;
}; };

react-ui/src/pages/Dataset/index.jsx → react-ui/src/pages/Dataset/index.tsx View File

@@ -1,5 +1,5 @@
import ResourcePage from './components/ResourcePage'; import ResourcePage from './components/ResourcePage';
import { ResourceType } from './types';
import { ResourceType } from './config';


const DatasetPage = () => { const DatasetPage = () => {
return <ResourcePage resourceType={ResourceType.Dataset} />; return <ResourcePage resourceType={ResourceType.Dataset} />;

+ 0
- 263
react-ui/src/pages/Dataset/intro.jsx View File

@@ -1,263 +0,0 @@
import KFIcon from '@/components/KFIcon';
import { ResourceType } from '@/pages/Dataset/types';
import {
deleteDatasetVersion,
getDatasetById,
getDatasetVersionIdList,
getDatasetVersionsById,
} from '@/services/dataset/index.js';
import { formatDate } from '@/utils/date';
import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Input, Select, Table, Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import AddVersionModal from './components/AddVersionModal';
import Styles from './intro.less';
const { Search } = Input;
const { TabPane } = Tabs;

const Dataset = () => {
const { message } = App.useApp();
const [formList, setFormList] = useState([]);
const [datasetDetailObj, setDatasetDetailObj] = useState({});
const [version, setVersion] = useState(null);
const [versionList, setVersionList] = useState([]);
const locationParams = useParams(); //新版本获取路由参数接口
const [searchParams] = useSearchParams();
const [wordList, setWordList] = useState([]);
const [activeTabKey, setActiveTabKey] = useState('1');
const isPublic = searchParams.get('isPublic') === 'true';

const getDatasetByDetail = () => {
getDatasetById(locationParams.id).then((ret) => {
console.log(ret);
setDatasetDetailObj(ret.data);
});
};
// 获取数据集版本
const getDatasetVersionList = () => {
getDatasetVersionsById(locationParams.id).then((ret) => {
console.log(ret);
if (ret.data && ret.data.length > 0) {
setVersionList(
ret.data.map((item) => {
return {
label: item,
value: item,
};
}),
);
setVersion(ret.data[0]);
getDatasetVersions({ version: ret.data[0], dataset_id: locationParams.id });
} else {
setVersion(null);
setWordList([]);
}
});
};
useEffect(() => {
getDatasetByDetail();
getDatasetVersionList();
return () => {};
}, []);
const showModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: ResourceType.Dataset,
resourceId: locationParams.id,
initialName: datasetDetailObj.name,
onOk: () => {
getDatasetVersionList();
close();
},
});
};

const deleteDataset = () => {
modalConfirm({
title: '删除后,该数据集版本将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteDatasetVersion({ dataset_id: locationParams.id, version }).then((ret) => {
getDatasetVersionList();
message.success('删除成功');
});
},
});
};
// 获取版本下的文件列表
const getDatasetVersions = (params) => {
getDatasetVersionIdList(params).then((res) => {
setWordList(res?.data?.content ?? []);
});
};

const handleExport = async () => {
const hide = message.loading('正在下载');
hide();
downLoadZip(`/api/mmp/dataset/downloadAllFiles`, { dataset_id: locationParams.id, version });
};

const downloadAlone = (e, record) => {
console.log(record);
const hide = message.loading('正在下载');
hide();
downLoadZip(`/api/mmp/dataset/download/${record.id}`);
};

const handleChange = (value) => {
console.log(value);
if (value) {
getDatasetVersions({ version: value, dataset_id: locationParams.id });
setVersion(value);
} else {
setVersion(null);
}
};

const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render(text, record, index) {
return <span>{(pageOption.current.page - 1) * 10 + index + 1}</span>;
},
// render: (text, record, index) => `${((curPage-1)*10)+(index+1)}`,
},
{
title: '文件名称',
dataIndex: 'file_name',
key: 'file_name',
render: (text, record) => <a onClick={(e) => downloadAlone(e, record)}>{text}</a>,
},
{
title: '版本号',
dataIndex: 'version',
key: 'version',
},
{
title: '文件大小',
dataIndex: 'file_size',
key: 'file_size',
},
{
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
render: (text) => <span>{formatDate(text)}</span>,
},
{
title: '操作',
dataIndex: 'option',
width: '100px',
key: 'option',
render: (_, record) => [
<Button
type="link"
size="small"
key="download"
icon={<KFIcon type="icon-xiazai" />}
onClick={(e) => downloadAlone(e, record)}
>
下载
</Button>,
],
},
];
const pageOption = useRef({ page: 1, size: 10 });

// 当前页面切换
const paginationChange = async (current, size) => {
console.log('page', current, size);
pageOption.current = {
page: current,
size: size,
};
// getList()
};
return (
<div className={Styles.datasetBox}>
<div className={Styles.datasetIntroTopBox}>
<span style={{ color: '#1d1d20', fontSize: '20px' }}>{datasetDetailObj.name}</span>
<div className={Styles.smallTagBox}>
<div className={Styles.tagItem}>数据集 id:{datasetDetailObj.id}</div>
<div className={Styles.tagItem}>{datasetDetailObj.dataset_type_name || '...'}</div>
<div className={Styles.tagItem}>{datasetDetailObj.dataset_tag_name || '...'}</div>
</div>
</div>
<div className={Styles.datasetIntroCneterBox}>
<Tabs activeKey={activeTabKey} onChange={(key) => setActiveTabKey(key)}>
<TabPane tab="数据集简介" key="1">
<div className={Styles.datasetIntroTitle}>简介</div>
<div className={Styles.datasetIntroText}>{datasetDetailObj.description}</div>
</TabPane>
<TabPane tab="数据集文件/版本" key="2">
<div className={Styles.dataListBox}>
<div>数据集文件列表</div>
<div className={Styles.dataButtonList}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{
width: 160,
}}
allowClear
value={version}
onChange={handleChange}
options={versionList}
/>
<Button
type="default"
className={Styles.plusButton}
onClick={showModal}
icon={<KFIcon type="icon-xinjian2" />}
>
创建新版本
</Button>
</div>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
{!isPublic && (
<Button
type="default"
className={Styles.plusButton}
style={{ margin: '0 20px 0 0' }}
onClick={deleteDataset}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
<Button
type="default"
disabled={!version}
className={Styles.plusButton}
style={{ margin: '0 20px 0 0' }}
onClick={handleExport}
icon={<KFIcon type="icon-xiazai" />}
>
下载
</Button>
</div>
</div>
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
{wordList.length > 0 && wordList[0].description
? '版本描述:' + wordList[0].description
: null}
</div>
<Table columns={columns} dataSource={wordList} pagination={false} rowKey="id" />
</div>
</TabPane>
</Tabs>
</div>
</div>
);
};
export default Dataset;

+ 0
- 82
react-ui/src/pages/Dataset/intro.less View File

@@ -1,82 +0,0 @@
.datasetIntroTopBox {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 110px;
margin-bottom: 10px;
padding: 25px 30px;
background-image: url(/assets/images/dataset-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

.smallTagBox {
display: flex;
align-items: center;
color: #1664ff;
font-size: 14px;
.tagItem {
margin-right: 20px;
padding: 4px 10px;
background: rgba(22, 100, 255, 0.1);
border-radius: 4px;
}
}
}
.dataListBox {
padding: 20px 30px;
color: #1d1d20;
font-size: 16px;
font-family: alibaba;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
.dataButtonList {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
margin: 24px 0 30px 0;
color: #575757;
font-size: 16px;
}
}
.datasetIntroCneterBox {
height: 77vh;
padding: 20px 30px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
}
.datasetIntroTitle {
margin: 37px 0 10px 0;
color: #1d1d20;
font-size: 15px;
}
.datasetIntroText {
margin-bottom: 30px;
color: #575757;
font-size: 14px;
}
.datasetBox {
background: #f9fafb;

:global {
.ant-tabs-top > .ant-tabs-nav {
margin: 0;
}
.ant-pagination {
text-align: right;
}
}
}

.plusButton {
margin: 0 18px 0 20px;
}

.tipContent {
margin-top: 5px;
color: #c73131;
}

+ 8
- 0
react-ui/src/pages/Dataset/intro.tsx View File

@@ -0,0 +1,8 @@
import ResourceIntro from '@/pages/Dataset/components/ResourceIntro';
import { ResourceType } from '@/pages/Dataset/config';

function DatasetIntro() {
return <ResourceIntro resourceType={ResourceType.Dataset} />;
}

export default DatasetIntro;

+ 1
- 1
react-ui/src/pages/DatasetPreparation/DatasetAnnotation/index.tsx View File

@@ -16,7 +16,7 @@ function DatasetAnnotation() {
}; };
return ( return (
<div className={styles.container}> <div className={styles.container}>
<iframe src="http://172.20.32.181:31213/label-studio" className={styles.frame}></iframe>
{iframeUrl && <iframe src={iframeUrl} className={styles.frame}></iframe>}
</div> </div>
); );
} }


+ 10
- 17
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -1,3 +1,5 @@
import ParameterInput from '@/components/ParameterInput';
import ParameterSelect from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import { PipelineNodeModelSerialize } from '@/types'; import { PipelineNodeModelSerialize } from '@/types';
@@ -122,15 +124,8 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
<TextArea disabled /> <TextArea disabled />
</Form.Item> </Form.Item>
{controlStrategyList.map((item) => ( {controlStrategyList.map((item) => (
<Form.Item
key={item.key}
name={['control_strategy', item.key]}
label={item.value.label}
getValueProps={(e) => {
return { value: e.showValue || e.value };
}}
>
<Input disabled />
<Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}>
<ParameterInput disabled />
</Form.Item> </Form.Item>
))} ))}
<div className={styles['experiment-parameter__title']}> <div className={styles['experiment-parameter__title']}>
@@ -142,11 +137,12 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
name={['in_parameters', item.key]} name={['in_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'} label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]} rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.showValue || e.value };
}}
> >
<Input disabled />
{item.value.type === 'select' ? (
<ParameterSelect disabled />
) : (
<ParameterInput disabled />
)}
</Form.Item> </Form.Item>
))} ))}
<div className={styles['experiment-parameter__title']}> <div className={styles['experiment-parameter__title']}>
@@ -158,11 +154,8 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
name={['out_parameters', item.key]} name={['out_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'} label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]} rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.showValue || e.value };
}}
> >
<Input disabled />
<ParameterInput disabled />
</Form.Item> </Form.Item>
))} ))}
</Form> </Form>


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

@@ -35,4 +35,9 @@
} }
} }
} }

&__empty {
margin-top: 10px;
text-align: center;
}
} }

+ 30
- 26
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -23,34 +23,38 @@ function ExperimentResult({ results }: ExperimentResultProps) {
return ( return (
<div className={styles['experiment-result']}> <div className={styles['experiment-result']}>
<div className={styles['experiment-result__content']}> <div className={styles['experiment-result__content']}>
{results?.map((item) => (
<div key={item.name} className={styles['experiment-result__item']}>
<div className={styles['experiment-result__item__name']}>
<span>{item.name}</span>
<Button
size="small"
type="link"
onClick={() => {
exportResult(item.path);
}}
>
下载
</Button>
{/* <a style={{ marginRight: '10px' }}>导出到模型库</a>
{results && results.length > 0 ? (
results.map((item) => (
<div key={item.name} className={styles['experiment-result__item']}>
<div className={styles['experiment-result__item__name']}>
<span>{item.name}</span>
<Button
size="small"
type="link"
onClick={() => {
exportResult(item.path);
}}
>
下载
</Button>
{/* <a style={{ marginRight: '10px' }}>导出到模型库</a>
<a style={{ marginRight: '10px' }}>导出到数据集</a> */} <a style={{ marginRight: '10px' }}>导出到数据集</a> */}
</div>
<div style={{ margin: '15px 0' }} className={styles['experiment-result__item__file']}>
<span>文件名称</span>
<span>文件大小</span>
</div>
{item.value?.map((ele) => (
<div className={styles['experiment-result__item__file']} key={ele.name}>
<span>{ele.name}</span>
<span>{ele.size}</span>
</div> </div>
))}
</div>
))}
<div style={{ margin: '15px 0' }} className={styles['experiment-result__item__file']}>
<span>文件名称</span>
<span>文件大小</span>
</div>
{item.value?.map((ele) => (
<div className={styles['experiment-result__item__file']} key={ele.name}>
<span>{ele.name}</span>
<span>{ele.size}</span>
</div>
))}
</div>
))
) : (
<div className={styles['experiment-result__empty']}>暂无结果</div>
)}
</div> </div>
</div> </div>
); );


+ 5
- 0
react-ui/src/pages/Experiment/components/LogGroup/index.less View File

@@ -21,8 +21,13 @@
color: white; color: white;
font-size: 14px; font-size: 14px;
white-space: pre-line; white-space: pre-line;
text-align: left;
word-break: break-all; word-break: break-all;
background: #19253b; background: #19253b;

&--empty {
text-align: center;
}
} }


&__more-button { &__more-button {


+ 57
- 3
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -10,6 +10,7 @@ import { ExperimentLog } from '@/pages/Experiment/training/props';
import { getExperimentPodsLog } from '@/services/experiment/index.js'; import { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styles from './index.less'; import styles from './index.less';


@@ -22,6 +23,21 @@ type Log = {
log_content: string; // 日志内容 log_content: string; // 日志内容
}; };


// 滚动到底部
const scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0];
if (element) {
if (smooth) {
element.scrollTo({
top: element.scrollHeight,
behavior: 'smooth',
});
} else {
element.scrollTo({ top: element.scrollHeight });
}
}
};

function LogGroup({ function LogGroup({
log_type = 'normal', log_type = 'normal',
pod_name = '', pod_name = '',
@@ -32,8 +48,11 @@ function LogGroup({
const [collapse, setCollapse] = useState(true); const [collapse, setCollapse] = useState(true);
const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); const [logList, setLogList, logListRef] = useStateRef<Log[]>([]);
const [completed, setCompleted] = useState(false); const [completed, setCompleted] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);


useEffect(() => { useEffect(() => {
scrollToBottom(false);
if (status === ExperimentStatus.Running) { if (status === ExperimentStatus.Running) {
const timerId = setInterval(() => { const timerId = setInterval(() => {
requestExperimentPodsLog(); requestExperimentPodsLog();
@@ -44,6 +63,24 @@ function LogGroup({
} }
}, []); }, []);


useEffect(() => {
const mouseDown = () => {
setIsMouseDown(true);
};

const mouseUp = () => {
setIsMouseDown(false);
};

document.addEventListener('mousedown', mouseDown);
document.addEventListener('mouseup', mouseUp);

return () => {
document.removeEventListener('mousedown', mouseDown);
document.removeEventListener('mouseup', mouseUp);
};
}, []);

// 请求日志 // 请求日志
const requestExperimentPodsLog = async () => { const requestExperimentPodsLog = async () => {
const list = logListRef.current; const list = logListRef.current;
@@ -54,9 +91,18 @@ function LogGroup({
}; };
const res = await getExperimentPodsLog(params); const res = await getExperimentPodsLog(params);
const { log_detail } = res.data; const { log_detail } = res.data;
if (log_detail && log_detail.log_content) {
if (log_detail) {
setLogList((oldList) => oldList.concat(log_detail)); setLogList((oldList) => oldList.concat(log_detail));
} else {

if (!isMouseDownRef.current && log_detail.log_content) {
setTimeout(() => {
scrollToBottom();
}, 100);
}
}

// 判断是否日志是否加载完成
if (!log_detail?.log_content) {
setCompleted(true); setCompleted(true);
} }
}; };
@@ -96,7 +142,15 @@ function LogGroup({
{collapse ? <DownOutlined /> : <UpOutlined />} {collapse ? <DownOutlined /> : <UpOutlined />}
</div> </div>
)} )}
{showLog && <div className={styles['log-group__detail']}>{logText}</div>}
{showLog && (
<div
className={classNames(styles['log-group__detail'], {
[styles['log-group__detail--empty']]: !logText,
})}
>
{logText ? logText : '暂无日志'}
</div>
)}
<div className={styles['log-group__more-button']}> <div className={styles['log-group__more-button']}>
{showMoreBtn && ( {showMoreBtn && (
<Button <Button


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

@@ -1,3 +1,13 @@
.log-list { .log-list {
padding: 8px; padding: 8px;

&__empty {
padding: 15px;
color: white;
font-size: 14px;
white-space: pre-line;
text-align: center;
word-break: break-all;
background: #19253b;
}
} }

+ 5
- 3
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -11,9 +11,11 @@ type LogListProps = {
function LogList({ list = [], status }: LogListProps) { function LogList({ list = [], status }: LogListProps) {
return ( return (
<div className={styles['log-list']}> <div className={styles['log-list']}>
{list.map((v) => (
<LogGroup key={v.pod_name} {...v} status={status} />
))}
{list.length > 0 ? (
list.map((v) => <LogGroup key={v.pod_name} {...v} status={status} />)
) : (
<div className={styles['log-list__empty']}>暂无日志</div>
)}
</div> </div>
); );
} }


+ 3
- 3
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less View File

@@ -8,15 +8,15 @@
font-size: 15px; font-size: 15px;


&--running { &--running {
color: #6ac21d;
color: @success-color;
} }
&--failed { &--failed {
color: #df6d6d;
color: @error-color;
} }
} }
&__icon { &__icon {
width: 14px; width: 14px;
color: #6ac21d;
color: @success-color;
cursor: pointer; cursor: pointer;


& + & { & + & {


+ 4
- 4
react-ui/src/pages/Experiment/components/ViewParamsModal/index.less View File

@@ -11,8 +11,8 @@
margin-bottom: 15px; margin-bottom: 15px;


&_label { &_label {
width: 120px;
color: #1d1d20;
width: 180px;
color: @text-color;
font-size: 15px; font-size: 15px;
} }
&_value { &_value {
@@ -20,8 +20,8 @@
width: 100px; width: 100px;
margin-left: 15px; margin-left: 15px;
padding: 10px 20px; padding: 10px 20px;
color: #1d1d20;
font-size: 15px;
color: @text-color;
font-size: @font-size;
line-height: 20px; line-height: 20px;
background: #f6f6f6; background: #f6f6f6;
border: 1px solid #e0e0e1; border: 1px solid #e0e0e1;


+ 9
- 8
react-ui/src/pages/Experiment/index.jsx View File

@@ -18,7 +18,7 @@ import themes from '@/styles/theme.less';
import { elapsedTime, formatDate } from '@/utils/date'; import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Space, Table } from 'antd';
import { App, Button, ConfigProvider, Space, Table, Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -84,7 +84,6 @@ function Experiment() {
// 获取实验实例 // 获取实验实例
const getQueryByExperiment = (val) => { const getQueryByExperiment = (val) => {
getQueryByExperimentId(val).then((ret) => { getQueryByExperimentId(val).then((ret) => {
console.log(val);
setExpandedRowKeys(val); setExpandedRowKeys(val);
if (ret && ret.data && ret.data.length > 0) { if (ret && ret.data && ret.data.length > 0) {
try { try {
@@ -162,7 +161,6 @@ function Experiment() {
}; };
const expandChange = (e, record) => { const expandChange = (e, record) => {
clearExperimentInTimers(); clearExperimentInTimers();
console.log(e, record);
if (record.id === expandedRowKeys) { if (record.id === expandedRowKeys) {
setExpandedRowKeys(null); setExpandedRowKeys(null);
} else { } else {
@@ -238,7 +236,6 @@ function Experiment() {
}; };
// 当前页面切换 // 当前页面切换
const paginationChange = async (current, size) => { const paginationChange = async (current, size) => {
console.log('page', current, size);
pageOption.current = { pageOption.current = {
page: current, page: current,
size: size, size: size,
@@ -279,14 +276,14 @@ function Experiment() {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
render: (text) => <div>{text}</div>, render: (text) => <div>{text}</div>,
width: '20%',
width: '16%',
}, },
{ {
title: '关联流水线名称', title: '关联流水线名称',
dataIndex: 'workflow_name', dataIndex: 'workflow_name',
key: 'workflow_name', key: 'workflow_name',
render: (text, record) => <a onClick={(e) => routeToEdit(e, record)}>{text}</a>, render: (text, record) => <a onClick={(e) => routeToEdit(e, record)}>{text}</a>,
width: '20%',
width: '16%',
}, },
{ {
title: '实验描述', title: '实验描述',
@@ -443,13 +440,17 @@ function Experiment() {
<div style={{ width: '50%' }}> <div style={{ width: '50%' }}>
{elapsedTime(item.create_time, item.finish_time)} {elapsedTime(item.create_time, item.finish_time)}
</div> </div>
<div style={{ width: '50%' }}>{formatDate(item.create_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>
<div className={Styles.statusBox}> <div className={Styles.statusBox}>
<img <img
style={{ width: '17px', marginRight: '7px' }} style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[item.status]?.icon} src={experimentStatusInfo[item.status]?.icon}
/>{' '}
/>
<span <span
style={{ color: experimentStatusInfo[item.status]?.color }} style={{ color: experimentStatusInfo[item.status]?.color }}
className={Styles.statusIcon} className={Styles.statusIcon}


+ 8
- 3
react-ui/src/pages/Experiment/index.less View File

@@ -36,17 +36,21 @@
} }


.index { .index {
width: calc((100% + 32px + 33px) / 5);
width: calc((100% + 32px + 33px) / 6.25);
} }


.tensorBoard { .tensorBoard {
width: calc((100% + 32px + 33px) / 5);
width: calc((100% + 32px + 33px) / 6.25);
} }


.description { .description {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-items: center;

.startTime {
.singleLine();
}
} }


.status { .status {
@@ -80,8 +84,9 @@
.statusBox:hover .statusIcon { .statusBox:hover .statusIcon {
visibility: visible; visibility: visible;
} }

.experimentBox { .experimentBox {
height: calc(100% - 20px);
height: 100%;
.experimentTable { .experimentTable {
height: calc(100% - 60px); height: calc(100% - 60px);
:global { :global {


+ 10
- 8
react-ui/src/pages/Experiment/status.ts View File

@@ -1,3 +1,5 @@
import themes from '@/styles/theme.less';

export interface StatusInfo { export interface StatusInfo {
label: string; label: string;
color: string; color: string;
@@ -18,42 +20,42 @@ export enum ExperimentStatus {
export const experimentStatusInfo: Record<ExperimentStatus, StatusInfo | undefined> = { export const experimentStatusInfo: Record<ExperimentStatus, StatusInfo | undefined> = {
Running: { Running: {
label: '运行中', label: '运行中',
color: '#1664ff',
color: themes.primaryColor,
icon: '/assets/images/running-icon.png', icon: '/assets/images/running-icon.png',
}, },
Succeeded: { Succeeded: {
label: '成功', label: '成功',
color: '#63a728',
color: themes.successColor,
icon: '/assets/images/success-icon.png', icon: '/assets/images/success-icon.png',
}, },
Pending: { Pending: {
label: '等待中', label: '等待中',
color: '#f981eb',
color: themes.pendingColor,
icon: '/assets/images/pending-icon.png', icon: '/assets/images/pending-icon.png',
}, },
Failed: { Failed: {
label: '失败', label: '失败',
color: '#c73131',
color: themes.errorColor,
icon: '/assets/images/fail-icon.png', icon: '/assets/images/fail-icon.png',
}, },
Error: { Error: {
label: '错误', label: '错误',
color: '#c73131',
color: themes.errorColor,
icon: '/assets/images/fail-icon.png', icon: '/assets/images/fail-icon.png',
}, },
Terminated: { Terminated: {
label: '终止', label: '终止',
color: '#8a8a8a',
color: themes.abortColor,
icon: '/assets/images/omitted-icon.png', icon: '/assets/images/omitted-icon.png',
}, },
Skipped: { Skipped: {
label: '未执行', label: '未执行',
color: '#8a8a8a',
color: themes.abortColor,
icon: '/assets/images/omitted-icon.png', icon: '/assets/images/omitted-icon.png',
}, },
Omitted: { Omitted: {
label: '未执行', label: '未执行',
color: '#8a8a8a',
color: themes.abortColor,
icon: '/assets/images/omitted-icon.png', icon: '/assets/images/omitted-icon.png',
}, },
}; };

+ 43
- 81
react-ui/src/pages/Experiment/training/index.jsx View File

@@ -1,12 +1,13 @@
import { useStateRef, useVisible } from '@/hooks'; import { useStateRef, useVisible } from '@/hooks';
import { getExperimentIns } from '@/services/experiment/index.js'; import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString } from '@/utils';
import { elapsedTime, formatDate } from '@/utils/date'; import { elapsedTime, formatDate } from '@/utils/date';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { Button } from 'antd'; import { Button } from 'antd';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { s8 } from '../../../utils';
import ParamsModal from '../components/ViewParamsModal'; import ParamsModal from '../components/ViewParamsModal';
import { experimentStatusInfo } from '../status'; import { experimentStatusInfo } from '../status';
import styles from './index.less'; import styles from './index.less';
@@ -22,27 +23,22 @@ function ExperimentText() {
const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);


const graphRef = useRef(); const graphRef = useRef();
const onDragEnd = (val) => {
console.log(val, 'eee');
const _x = val.x;
const _y = val.y;
const point = graph.getPointByClient(_x, _y);
let model = {};
// 元模型
model = {
...val,
x: point.x,
y: point.y,
id: val.component_name + '-' + s8(),
isCluster: false,
};
graph.addItem('node', model, true);
};
const handlerClick = (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
}
};
// const onDragEnd = (val) => {
// console.log(val, 'eee');
// const _x = val.x;
// const _y = val.y;
// const point = graph.getPointByClient(_x, _y);
// let model = {};
// // 元模型
// model = {
// ...val,
// x: point.x,
// y: point.y,
// id: val.component_name + '-' + s8(),
// isCluster: false,
// };
// graph.addItem('node', model, true);
// };
const getGraphData = (data) => { const getGraphData = (data) => {
if (graph) { if (graph) {
graph.data(data); graph.data(data);
@@ -89,32 +85,6 @@ function ExperimentText() {
}, []); }, []);


const initGraph = () => { const initGraph = () => {
const fittingString = (str, maxWidth, fontSize) => {
const ellipsis = '...';
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`;
}
});
return res;
};
// 获取文本的长度
const getTextSize = (str, maxWidth, fontSize) => {
let width = G6.Util.getTextSize(str, fontSize)[0];
return width > maxWidth ? maxWidth : width;
};
G6.registerNode( G6.registerNode(
'rect-node', 'rect-node',
{ {
@@ -129,7 +99,6 @@ function ExperimentText() {
); );
}, },
afterDraw(cfg, group) { afterDraw(cfg, group) {
// console.log(group, cfg, 12312);
const image = group.addShape('image', { const image = group.addShape('image', {
attrs: { attrs: {
x: -45, x: -45,
@@ -158,7 +127,6 @@ function ExperimentText() {
} }
const bbox = group.getBBox(); const bbox = group.getBBox();
const anchorPoints = this.getAnchorPoints(cfg); const anchorPoints = this.getAnchorPoints(cfg);
// console.log(anchorPoints);
anchorPoints.forEach((anchorPos, i) => { anchorPoints.forEach((anchorPos, i) => {
group.addShape('circle', { group.addShape('circle', {
attrs: { attrs: {
@@ -179,19 +147,19 @@ function ExperimentText() {


// response the state changes and show/hide the link-point circles // response the state changes and show/hide the link-point circles
setState(name, value, item) { setState(name, value, item) {
const 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];
if (name === 'hover') {
if (value) {
shape.attr('stroke', themes['primaryColor']);
} else {
shape.attr('stroke', '#fff');
}
}
}, },
}, },
'rect', 'rect',
); );
console.log(graphRef, 'graphRef');
graph = new G6.Graph({ graph = new G6.Graph({
container: graphRef.current, container: graphRef.current,
grid: true, grid: true,
@@ -209,10 +177,6 @@ function ExperimentText() {
if (e.target.get('name') === 'anchor-point') return false; if (e.target.get('name') === 'anchor-point') return false;
return true; return true;
}, },
// shouldEnd: e => {
// console.log(e);
// return false;
// },
}, },
// config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles
'drag-canvas', 'drag-canvas',
@@ -237,7 +201,6 @@ function ExperimentText() {
style: { style: {
fill: '#000', fill: '#000',
fontSize: 10, fontSize: 10,

cursor: 'pointer', cursor: 'pointer',
x: -20, x: -20,
y: 0, y: 0,
@@ -252,17 +215,6 @@ function ExperimentText() {
lineWidth: 0.5, lineWidth: 0.5,
}, },
}, },
nodeStateStyles: {
nodeSelected: {
fill: 'red',
shadowColor: 'red',
stroke: 'red',
'text-shape': {
fill: 'red',
stroke: 'red',
},
},
},
defaultEdge: { defaultEdge: {
// type: 'quadratic', // type: 'quadratic',
type: 'cubic-vertical', type: 'cubic-vertical',
@@ -308,15 +260,25 @@ function ExperimentText() {
// linkCenter: true, // linkCenter: true,
fitView: true, fitView: true,
minZoom: 0.5, minZoom: 0.5,
maxZoom: 3,
fitViewPadding: [320, 320, 220, 320],
maxZoom: 5,
fitViewPadding: 300,
});
graph.on('node:click', (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
}
});
graph.on('node:mouseenter', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
}); });
graph.on('node:click', handlerClick);
window.onresize = () => { window.onresize = () => {
if (!graph || graph.get('destroyed')) return; if (!graph || graph.get('destroyed')) return;
if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight)
return;
graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20);
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
}; };
}; };
return ( return (


+ 2
- 1
react-ui/src/pages/Experiment/training/props.tsx View File

@@ -109,7 +109,8 @@ const Props = forwardRef((_, ref) => {
// 获取实验日志和实验结果 // 获取实验日志和实验结果
setExperimentLogList([]); setExperimentLogList([]);
setExperimentResults([]); setExperimentResults([]);
if (e.item && e.item.getModel()) {
// 如果已经运行到了
if (e.item?.getModel()?.component_id) {
const model = e.item.getModel(); const model = e.item.getModel();
const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6; const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6;
const params = { const params = {


+ 8
- 15
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -20,7 +20,7 @@ import { formatDate } from '@/utils/date';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage'; import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useNavigate, useParams, useSearchParams } from '@umijs/max';
import { useNavigate, useParams } from '@umijs/max';
import { import {
App, App,
Button, Button,
@@ -33,7 +33,7 @@ import {
type TableProps, type TableProps,
} from 'antd'; } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import MirrorStatusCell from '../components/MirrorStatusCell'; import MirrorStatusCell from '../components/MirrorStatusCell';
import styles from './index.less'; import styles from './index.less';


@@ -42,6 +42,7 @@ type MirrorInfoData = {
description?: string; description?: string;
version_count?: string; version_count?: string;
create_time?: string; create_time?: string;
image_type?: number;
}; };


type MirrorVersionData = { type MirrorVersionData = {
@@ -56,7 +57,6 @@ type MirrorVersionData = {
function MirrorInfo() { function MirrorInfo() {
const navigate = useNavigate(); const navigate = useNavigate();
const urlParams = useParams(); const urlParams = useParams();
const [searchParams] = useSearchParams();
const [cacheState, setCacheState] = useCacheState(); const [cacheState, setCacheState] = useCacheState();
const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({}); const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({});
const [tableData, setTableData] = useState<MirrorVersionData[]>([]); const [tableData, setTableData] = useState<MirrorVersionData[]>([]);
@@ -69,7 +69,7 @@ function MirrorInfo() {
}, },
); );
const { message } = App.useApp(); const { message } = App.useApp();
const isPublic = searchParams.get('isPublic') === 'true';
const isPublic = useMemo(() => mirrorInfo.image_type === 1, [mirrorInfo]);


useEffect(() => { useEffect(() => {
getMirrorInfo(); getMirrorInfo();
@@ -84,14 +84,7 @@ function MirrorInfo() {
const id = Number(urlParams.id); const id = Number(urlParams.id);
const [res] = await to(getMirrorInfoReq(id)); const [res] = await to(getMirrorInfoReq(id));
if (res && res.data) { if (res && res.data) {
const { name = '', description = '', version_count = '', create_time: time } = res.data;
const create_time = formatDate(time);
setMirrorInfo({
name,
description,
version_count,
create_time,
});
setMirrorInfo(res.data);
} }
}; };


@@ -258,7 +251,7 @@ function MirrorInfo() {
<Col span={10}> <Col span={10}>
<div className={styles['mirror-info__basic__item']}> <div className={styles['mirror-info__basic__item']}>
<div className={styles['label']}>创建时间:</div> <div className={styles['label']}>创建时间:</div>
<div className={styles['value']}>{mirrorInfo.create_time}</div>
<div className={styles['value']}>{formatDate(mirrorInfo.create_time)}</div>
</div> </div>
</Col> </Col>
</Row> </Row>
@@ -270,7 +263,7 @@ function MirrorInfo() {
></SubAreaTitle> ></SubAreaTitle>
{!isPublic && ( {!isPublic && (
<Button <Button
style={{ marginRight: 0, marginLeft: 'auto' }}
style={{ marginLeft: 'auto' }}
type="default" type="default"
onClick={createMirrorVersion} onClick={createMirrorVersion}
icon={<KFIcon type="icon-xinjian2" />} icon={<KFIcon type="icon-xinjian2" />}
@@ -279,7 +272,7 @@ function MirrorInfo() {
</Button> </Button>
)} )}
<Button <Button
style={{ marginLeft: '20px' }}
style={{ marginLeft: isPublic ? 'auto' : '20px', marginRight: 0 }}
type="default" type="default"
onClick={getMirrorVersionList} onClick={getMirrorVersionList}
icon={<KFIcon type="icon-shuaxin" />} icon={<KFIcon type="icon-shuaxin" />}


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

@@ -125,7 +125,7 @@ function MirrorList() {


// 查看详情 // 查看详情
const toDetail = (record: MirrorData) => { const toDetail = (record: MirrorData) => {
navigate(`/dataset/mirror/${record.id}?isPublic=${activeTab === CommonTabKeys.Public}`);
navigate(`/dataset/mirror/${record.id}`);
setCacheState({ setCacheState({
activeTab, activeTab,
pagination, pagination,


+ 17
- 0
react-ui/src/pages/Model/components/GraphLegand/index.less View File

@@ -0,0 +1,17 @@
.graph-legend {
&__item {
margin-right: 20px;
color: @text-color;
font-size: @font-size-content;

&:last-child {
margin-right: 0;
}

&__name {
margin-left: 10px;
color: @text-color-secondary;
font-size: @font-size-content;
}
}
}

+ 55
- 0
react-ui/src/pages/Model/components/GraphLegand/index.tsx View File

@@ -0,0 +1,55 @@
import { Flex } from 'antd';
import styles from './index.less';

type GraphLegandData = {
name: string;
color: string;
radius: number;
fill: boolean;
};

type GraphLegandProps = {
style?: React.CSSProperties;
};

function GraphLegand({ style }: GraphLegandProps) {
const legends: GraphLegandData[] = [
{
name: '父模型',
color: '#76b1ff',
radius: 2,
fill: true,
},
{
name: '当前模型',
color: '#1664ff',
radius: 2,
fill: true,
},
{
name: '衍生模型',
color: '#b7cfff',
radius: 2,
fill: true,
},
];
return (
<Flex align="center" className={styles['graph-legend']} style={style}>
{legends.map((item) => (
<Flex align="center" key={item.name} className={styles['graph-legend__item']}>
<div
style={{
width: '16px',
height: '12px',
borderRadius: item.radius,
backgroundColor: item.color,
}}
></div>
<div className={styles['graph-legend__item__name']}>{item.name}</div>
</Flex>
))}
</Flex>
);
}

export default GraphLegand;

+ 18
- 0
react-ui/src/pages/Model/components/ModelEvolution/index.less View File

@@ -0,0 +1,18 @@
.model-evolution {
width: 100%;
height: 100%;
background-color: white;

&__top {
padding: 30px 0;
color: @text-color;
font-size: @font-size-content;
}

&__graph {
height: calc(100% - 92px);
background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}
}

+ 521
- 0
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -0,0 +1,521 @@
import { useEffectWhen } from '@/hooks';
import { ResourceVersionData } from '@/pages/Dataset/config';
import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less';
import { changePropertyName, fittingString } from '@/utils';
import { to } from '@/utils/promise';
import G6, {
EdgeConfig,
G6GraphEvent,
Graph,
GraphData,
LayoutConfig,
NodeConfig,
TreeGraphData,
Util,
} from '@antv/g6';
// @ts-ignore
import Hierarchy from '@antv/hierarchy';
import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react';
import GraphLegand from '../GraphLegand';
import NodeTooltips from '../NodeTooltips';
import styles from './index.less';

const nodeWidth = 98;
const nodeHeight = 58;
const vGap = 58;
const hGap = 58;

enum NodeType {
current = 'current',
parent = 'parent',
children = 'children',
project = 'project',
trainDataset = 'trainDataset',
testDataset = 'testDataset',
}

type TrainTask = {
ins_id: number;
name: string;
task_id: string;
};

interface TrainDataset extends NodeConfig {
dataset_id: number;
dataset_name: string;
dataset_version: string;
model_type: NodeType;
}

interface ProjectDependency extends NodeConfig {
url: string;
name: string;
branch: string;
model_type: NodeType;
}

type ModalDetail = {
name: string;
available_range: number;
file_name: string;
file_size: string;
description: string;
model_type_name: string;
model_tag_name: string;
create_time: string;
};

interface ModelDepsAPIData {
current_model_id: number;
version: string;
exp_ins_id: number;
model_type: NodeType;
current_model_name: string;
project_dependency: ProjectDependency;
test_dataset: TrainDataset[];
train_dataset: TrainDataset[];
train_task: TrainTask;
model_version_dependcy_vo: ModalDetail;
children_models: ModelDepsAPIData[];
parent_models: ModelDepsAPIData[];
}

export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData {
children: ModelDepsData[];
}

// 规范化子数据
function normalizeChildren(data: ModelDepsData[]) {
if (Array.isArray(data)) {
data.forEach((item) => {
item.model_type = NodeType.children;
item.id = `$M_${item.current_model_id}_${item.version}`;
item.label = getLabel(item);
item.style = getStyle(NodeType.children);
normalizeChildren(item.children);
});
}
}

// 获取 label
function getLabel(node: { current_model_name: string; version: string }) {
return (
fittingString(`${node.current_model_name}`, 87, 8) +
'\n' +
fittingString(`${node.version}`, 87, 8)
);
}

// 获取 style
function getStyle(model_type: NodeType) {
let fill = '';
switch (model_type) {
case NodeType.current:
fill = '#1664ff';
break;
case NodeType.parent:
fill = '#76b1ff';
break;
case NodeType.children:
fill = '#b7cfff';
break;
case NodeType.project:
fill = '#FA8C16';
break;
case NodeType.trainDataset:
fill = '#ff0000';
break;
case NodeType.testDataset:
fill = '#ff00ff';
break;
default:
break;
}
return {
fill,
};
}

// 将后台返回的数据转换成树形数据
function normalizeTreeData(apiData: ModelDepsAPIData, currentNodeName: string): ModelDepsData {
// 将 children_models 转换成 children
let normalizedData = changePropertyName(apiData, {
children_models: 'children',
}) as ModelDepsData;

// 设置当前模型的数据
normalizedData.model_type = NodeType.current;
normalizedData.current_model_name = currentNodeName;
normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
normalizedData.label = getLabel(normalizedData);
normalizedData.style = getStyle(NodeType.current);

normalizeChildren(normalizedData.children as ModelDepsData[]);

// 将 parent_models 转换成树形结构
let parent_models = normalizedData.parent_models || [];
while (parent_models.length > 0) {
const parent = parent_models[0];
normalizedData = {
...parent,
model_type: NodeType.parent,
id: `$M_${parent.current_model_id}_${parent.version}`,
label: getLabel(parent),
style: getStyle(NodeType.parent),
children: [
{
...normalizedData,
parent_models: [],
},
],
};
parent_models = normalizedData.parent_models || [];
}
return normalizedData;
}

// 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据
function getGraphData(data: ModelDepsData): GraphData {
const config = {
direction: 'LR',
getHeight: () => nodeHeight,
getWidth: () => nodeWidth,
getVGap: () => vGap / 2,
getHGap: () => hGap / 2,
};

// 树形布局计算出坐标
const treeLayoutData: LayoutConfig = Hierarchy['compactBox'](data, config);

const nodes: NodeConfig[] = [];
const edges: EdgeConfig[] = [];
Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => {
const data = node.data as ModelDepsData;
nodes.push({
...data,
x: node.x,
y: node.y,
});
if (parent) {
edges.push({
source: parent.id,
target: node.id,
});
}

// 当前模型显示数据集和项目
if (data.model_type === NodeType.current) {
const { project_dependency, train_dataset, test_dataset } = data;
train_dataset.forEach((item) => {
item.id = `$DTrain_${item.dataset_id}`;
item.model_type = NodeType.trainDataset;
item.type = 'ellipse';
item.label = fittingString(`${item.dataset_name}`, 87, 8);
item.style = getStyle(NodeType.trainDataset);
});
test_dataset.forEach((item) => {
item.id = `$DTest_${item.dataset_id}`;
item.model_type = NodeType.testDataset;
item.type = 'ellipse';
item.label = fittingString(item.dataset_name, 87, 8);
item.style = getStyle(NodeType.testDataset);
});

const len = train_dataset.length + test_dataset.length;
[...train_dataset, ...test_dataset].forEach((item, index) => {
const half = len / 2 - 0.5;
item.x = node.x! - (half - index) * (nodeWidth + hGap);
item.y = node.y! - nodeHeight - vGap;
nodes.push(item);
edges.push({
source: node.id,
target: item.id,
sourceAnchor: 2,
targetAnchor: 3,
type: 'cubic-vertical',
});
});

if (project_dependency?.url) {
project_dependency.id = `$P_${project_dependency.url}`;
project_dependency.model_type = NodeType.project;
project_dependency.type = 'rect';
project_dependency.size = [nodeHeight, nodeHeight];
project_dependency.label = fittingString(project_dependency.name, 48, 8);
project_dependency.style = getStyle(NodeType.project);
project_dependency.x = node.x;
project_dependency.y = node.y! + nodeHeight + vGap;
nodes.push(project_dependency);
edges.push({
source: node.id,
target: project_dependency.id,
sourceAnchor: 3,
targetAnchor: 2,
type: 'cubic-vertical',
});
}
}
});
return { nodes, edges };
}

type modeModelEvolutionProps = {
resourceId: number;
resourceName: string;
versionList: ResourceVersionData[];
version?: string;
isActive: boolean;
onVersionChange: (version: string) => void;
};

let graph: Graph;
function ModelEvolution({
resourceId,
resourceName,
versionList,
version,
isActive,
onVersionChange,
}: modeModelEvolutionProps) {
const graphRef = useRef<HTMLDivElement>(null);
const [showNodeTooltip, setShowNodeTooltip] = useState(false);
const [enterTooltip, setEnterTooltip] = useState(false);
const [nodeTooltipX, setNodeToolTipX] = useState(0);
const [nodeTooltipY, setNodeToolTipY] = useState(0);
const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined);

useEffect(() => {
initGraph();
const changSize = () => {
if (!graph || graph.get('destroyed')) return;
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
};

window.addEventListener('resize', changSize);
return () => {
window.removeEventListener('resize', changSize);
};
}, []);

useEffectWhen(
() => {
if (version) {
getModelAtlas();
} else {
clearGraphData();
}
},
[resourceId, version],
isActive,
);

// 初始化图
const initGraph = () => {
graph = new G6.Graph({
container: graphRef.current!,
width: graphRef.current!.clientWidth,
height: graphRef.current!.clientHeight,
fitView: true,
fitViewPadding: [50, 100, 50, 100],
minZoom: 0.5,
maxZoom: 5,
defaultNode: {
type: 'rect',
size: [nodeWidth, nodeHeight],
anchorPoints: [
[0, 0.5],
[1, 0.5],
[0.5, 0],
[0.5, 1],
],
style: {
fill: themes['primaryColor'],
lineWidth: 0,
radius: 6,
cursor: 'pointer',
},
labelCfg: {
position: 'center',
style: {
fill: '#ffffff',
fontSize: 8,
textAlign: 'center',
},
},
},
defaultEdge: {
type: 'cubic-horizontal',
labelCfg: {
autoRotate: true,
},
style: {
stroke: '#a2c1ff',
lineWidth: 1,
},
},
modes: {
default: [
'drag-canvas',
'zoom-canvas',
// {
// type: 'collapse-expand',
// onChange(item?: Item, collapsed?: boolean) {
// const data = item!.getModel();
// data.collapsed = collapsed;
// return true;
// },
// },
],
},
});

bindEvents();
};

// 绑定事件
const bindEvents = () => {
graph.on('node:mouseenter', (e: G6GraphEvent) => {
const nodeItem = e.item;
graph.setItemState(nodeItem, 'hover', true);

const model = nodeItem.getModel() as ModelDepsData;
const { x, y, model_type } = model;
if (
model_type === NodeType.project ||
model_type === NodeType.trainDataset ||
model_type === NodeType.testDataset
) {
return;
}
const point = graph.getCanvasByPoint(x!, y!);
const canvasWidth = graphRef.current!.clientWidth;
if (point.x + 300 > canvasWidth) {
point.x = canvasWidth - 300;
}
const zoom = graph.getZoom();
// 更加缩放,调整 tooltip 位置
const offsetY = (nodeHeight * zoom) / 4;

setHoverNodeData(model);
setNodeToolTipX(point.x);
// 92: 版本选择器的高度,296: tooltip的高度
setNodeToolTipY(point.y + 92 - 296 - offsetY);
setShowNodeTooltip(true);
});

graph.on('node:mouseleave', (e: G6GraphEvent) => {
const nodeItem = e.item;
graph.setItemState(nodeItem, 'hover', false);
setShowNodeTooltip(false);
});

graph.on('node:click', (e: G6GraphEvent) => {
const nodeItem = e.item;
const model = nodeItem.getModel();
const { model_type } = model;
const { origin } = location;
let url: string = '';
switch (model_type) {
case NodeType.children:
case NodeType.parent: {
const { current_model_id, version } = model as ModelDepsData;
url = `${origin}/dataset/model/${current_model_id}?tab=3&version=${version}`;
break;
}
case NodeType.project: {
const { url: projectUrl } = model as ProjectDependency;
url = projectUrl;
break;
}
case NodeType.trainDataset:
case NodeType.testDataset: {
const { dataset_id, dataset_version } = model as TrainDataset;
url = `${origin}/dataset/dataset/${dataset_id}?tab=2&version=${dataset_version}`;
break;
}
default:
break;
}

if (url) {
window.open(url, '_blank');
}
});

// 鼠标滚轮缩放时,隐藏 tooltip
graph.on('wheelzoom', () => {
setShowNodeTooltip(false);
setEnterTooltip(false);
});
};

const handleTooltipsMouseEnter = () => {
setEnterTooltip(true);
};

const handleTooltipsMouseLeave = () => {
setEnterTooltip(false);
};

// 获取模型依赖
const getModelAtlas = async () => {
const params = {
model_id: resourceId,
version,
};
const [res] = await to(getModelAtlasReq(params));
if (res && res.data) {
const data = normalizeTreeData(res.data, resourceName);
const graphData = getGraphData(data);

graph.data(graphData);
graph.render();
graph.fitView();
} else {
clearGraphData();
}
};

// 请求失败或者版本不存在时,清除图形
function clearGraphData() {
graph.data({
nodes: [],
edges: [],
});
graph.render();
graph.fitView();
}

return (
<div className={styles['model-evolution']}>
<Flex align="center" className={styles['model-evolution__top']}>
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
allowClear
onChange={onVersionChange}
options={versionList}
/>
<GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand>
</Flex>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
{(showNodeTooltip || enterTooltip) && (
<NodeTooltips
x={nodeTooltipX}
y={nodeTooltipY}
data={hoverNodeData!}
onMouseEnter={handleTooltipsMouseEnter}
onMouseLeave={handleTooltipsMouseLeave}
/>
)}
</div>
);
}

export default ModelEvolution;

+ 56
- 0
react-ui/src/pages/Model/components/NodeTooltips/index.less View File

@@ -0,0 +1,56 @@
.node-tooltips {
position: absolute;
top: -100px;
left: -300px;
width: 300px;
padding: 10px;
background: white;
border: 1px solid #eaeaea;
border-radius: 4px;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);

&__title {
margin: 10px 0;
color: @text-color;
font-weight: 500;
font-size: @font-size-content;
}

&__row {
display: flex;
align-items: center;
margin: 4px 0;
color: @text-color;
font-size: 14px;

&:first-child {
margin-top: 0;
}

&:last-child {
margin-bottom: 10px;
}

&__title {
display: inline-block;
width: 100px;
color: @text-color-secondary;
text-align: right;
}

&__value {
flex: 1;
min-width: 0;
color: @text-color;
font-weight: 500;
.singleLine();
}

&__link {
flex: 1;
min-width: 0;
font-weight: 500;
.singleLine();
}
}
}

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

@@ -0,0 +1,78 @@
import { formatDate } from '@/utils/date';
import { ModelDepsData } from '../ModelEvolution';
import styles from './index.less';

type NodeTooltipsProps = {
data: ModelDepsData;
x: number;
y: number;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};

function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) {
const gotoExperimentPage = () => {
if (data.train_task?.ins_id) {
const { origin } = location;
window.open(`${origin}/pipeline/experiment/144/${data.train_task.ins_id}`, '_blank');
}
};

return (
<div
className={styles['node-tooltips']}
style={{ left: `${x}px`, top: `${y}px` }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className={styles['node-tooltips__title']}>模型信息</div>
<div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型名称:</span>
<span className={styles['node-tooltips__row__value']}>{data.current_model_name}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型版本:</span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型框架:</span>
<span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.model_type_name || '--'}
</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型大小:</span>
<span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.file_size || '--'}
</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>创建时间:</span>
<span className={styles['node-tooltips__row__value']}>
{formatDate(data.model_version_dependcy_vo?.create_time)}
</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型权限:</span>
<span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.available_range === 1 ? '公开' : '私有'}
</span>
</div>
</div>
<div className={styles['node-tooltips__title']}>训练相关信息</div>
<div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>训练任务:</span>
{data.train_task?.name ? (
<a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}>
{data.train_task?.name}
</a>
) : null}
</div>
</div>
</div>
);
}

export default NodeTooltips;

react-ui/src/pages/Model/index.jsx → react-ui/src/pages/Model/index.tsx View File

@@ -1,5 +1,5 @@
import ResourcePage from '@/pages/Dataset/components/ResourcePage'; import ResourcePage from '@/pages/Dataset/components/ResourcePage';
import { ResourceType } from '@/pages/Dataset/types';
import { ResourceType } from '@/pages/Dataset/config';


const ModelPage = () => { const ModelPage = () => {
return <ResourcePage resourceType={ResourceType.Model} />; return <ResourcePage resourceType={ResourceType.Model} />;

+ 0
- 262
react-ui/src/pages/Model/intro.jsx View File

@@ -1,262 +0,0 @@
import KFIcon from '@/components/KFIcon';
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
import { ResourceType } from '@/pages/Dataset/types';
import {
deleteModelVersion,
getModelById,
getModelVersionIdList,
getModelVersionsById,
} from '@/services/dataset/index.js';
import { formatDate } from '@/utils/date';
import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Input, Select, Table, Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import Styles from './intro.less';
const { Search } = Input;
const { TabPane } = Tabs;

const Dataset = () => {
const [formList, setFormList] = useState([]);
const [datasetDetailObj, setDatasetDetailObj] = useState({});
const [version, setVersion] = useState(null);
const [versionList, setVersionList] = useState([]);
const locationParams = useParams(); //新版本获取路由参数接口
const [searchParams] = useSearchParams();
const [wordList, setWordList] = useState([]);
const { message } = App.useApp();
const isPublic = searchParams.get('isPublic') === 'true';

const getModelByDetail = () => {
getModelById(locationParams.id).then((ret) => {
console.log(ret);
setDatasetDetailObj(ret.data);
});
};
const getModelVersionsList = () => {
getModelVersionsById(locationParams.id).then((ret) => {
console.log(ret);
if (ret && ret.data && ret.data.length > 0) {
setVersionList(
ret.data.map((item) => {
return {
label: item,
value: item,
};
}),
);
setVersion(ret.data[0]);
getModelVersions({ version: ret.data[0], models_id: locationParams.id });
} else {
setVersion(null);
setWordList([]);
}
});
};
useEffect(() => {
getModelByDetail();
getModelVersionsList();
return () => {};
}, []);
const showModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: ResourceType.Model,
resourceId: locationParams.id,
initialName: datasetDetailObj.name,
onOk: () => {
getModelVersionsList();
close();
},
});
};

const deleteDataset = () => {
modalConfirm({
title: '删除后,该版本将不可恢复',
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',

onOk: () => {
deleteModelVersion({ models_id: locationParams.id, version }).then((ret) => {
getModelVersionsList();
message.success('删除成功');
});
},
});
};

const getModelVersions = (params) => {
getModelVersionIdList(params).then((ret) => {
setWordList(ret?.data?.content ?? []);
});
};
const handleExport = async () => {
const hide = message.loading('正在下载');
hide();
downLoadZip(`/api/mmp/models/downloadAllFiles`, { models_id: locationParams.id, version });
};
const downloadAlone = (e, record) => {
console.log(record);
const hide = message.loading('正在下载');
hide();
downLoadZip(`/api/mmp/models/download_model/${record.id}`);
};
const handleChange = (value) => {
console.log(value);
if (value) {
getModelVersions({ version: value, models_id: locationParams.id });
setVersion(value);
} else {
setVersion('');
}
};

const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render(text, record, index) {
return <span>{(pageOption.current.page - 1) * 10 + index + 1}</span>;
},
// render: (text, record, index) => `${((curPage-1)*10)+(index+1)}`,
},
{
title: '文件名称',
dataIndex: 'file_name',
key: 'file_name',
render: (text, record) => <a onClick={(e) => downloadAlone(e, record)}>{text}</a>,
},
{
title: '版本号',
dataIndex: 'version',
key: 'version',
},
{
title: '文件大小',
dataIndex: 'file_size',
key: 'file_size',
},
{
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
render: (text) => <span>{formatDate(text)}</span>,
},
{
title: '操作',
dataIndex: 'option',
width: '100px',
key: 'option',
render: (_, record) => [
<Button
type="link"
size="small"
key="download"
icon={<KFIcon type="icon-xiazai" />}
onClick={(e) => downloadAlone(e, record)}
>
下载
</Button>,
],
},
];
const pageOption = useRef({ page: 1, size: 10 });

// 当前页面切换
const paginationChange = async (current, size) => {
console.log('page', current, size);
pageOption.current = {
page: current,
size: size,
};
// getList()
};
return (
<div className={Styles.datasetBox}>
<div className={Styles.datasetIntroTopBox}>
<span style={{ color: '#1d1d20', fontSize: '20px' }}>{datasetDetailObj.name}</span>
<div className={Styles.smallTagBox}>
<div className={Styles.tagItem}>模型 id:{datasetDetailObj.id}</div>
<div className={Styles.tagItem}>{datasetDetailObj.model_type_name || '...'}</div>
<div className={Styles.tagItem}>{datasetDetailObj.model_tag_name || '...'}</div>
</div>
</div>
<div className={Styles.datasetIntroCneterBox}>
<Tabs defaultActiveKey="1">
<TabPane tab="模型简介" key="1">
<div className={Styles.datasetIntroTitle}>简介</div>
<div className={Styles.datasetIntroText}>{datasetDetailObj.description}</div>
</TabPane>
<TabPane tab="模型文件/版本" key="2">
<div className={Styles.dataListBox}>
<div>模型列表</div>
<div className={Styles.dataButtonList}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{
width: 160,
}}
value={version}
allowClear
onChange={handleChange}
options={versionList}
/>
<Button
type="default"
className={Styles.plusButton}
onClick={showModal}
icon={<KFIcon type="icon-xinjian2" />}
>
创建新版本
</Button>
</div>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
{!isPublic && (
<Button
type="default"
className={Styles.plusButton}
style={{ margin: '0 20px 0 0' }}
onClick={deleteDataset}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}

<Button
type="default"
className={Styles.plusButton}
disabled={!version}
style={{ margin: '0 20px 0 0' }}
onClick={handleExport}
icon={<KFIcon type="icon-xiazai" />}
>
下载
</Button>
</div>
</div>
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
{wordList.length > 0 && wordList[0].description
? '版本描述:' + wordList[0].description
: null}
</div>
<Table columns={columns} dataSource={wordList} pagination={false} rowKey="id" />
</div>
</TabPane>
</Tabs>
</div>
</div>
);
};
export default Dataset;

+ 0
- 80
react-ui/src/pages/Model/intro.less View File

@@ -1,80 +0,0 @@
.datasetIntroTopBox {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 110px;
margin-bottom: 10px;
padding: 25px 30px;
background-image: url(/assets/images/dataset-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

.smallTagBox {
display: flex;
align-items: center;
color: #1664ff;
font-size: 14px;
.tagItem {
margin-right: 20px;
padding: 4px 10px;
background: rgba(22, 100, 255, 0.1);
border-radius: 4px;
}
}
}
.dataListBox {
padding: 20px 30px;
color: #1d1d20;
font-size: 16px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
.dataButtonList {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
margin: 24px 0 30px 0;
color: #575757;
font-size: 16px;
}
}
.datasetIntroCneterBox {
height: 77vh;
padding: 20px 30px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
}
.datasetIntroTitle {
margin: 37px 0 10px 0;
color: #1d1d20;
font-size: 15px;
}
.datasetIntroText {
margin-bottom: 30px;
color: #575757;
font-size: 14px;
}
.datasetBox {
font-family: 'Alibaba';
background: #f9fafb;
:global {
.ant-tabs-top > .ant-tabs-nav {
margin: 0;
}
.ant-pagination {
text-align: right;
}
}
}
.plusButton {
margin: 0 18px 0 20px;
}

.tipContent {
margin-top: 5px;
color: #c73131;
}

+ 8
- 0
react-ui/src/pages/Model/intro.tsx View File

@@ -0,0 +1,8 @@
import ResourceIntro from '@/pages/Dataset/components/ResourceIntro';
import { ResourceType } from '@/pages/Dataset/config';

function ModelIntro() {
return <ResourceIntro resourceType={ResourceType.Model} />;
}

export default ModelIntro;

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

@@ -104,7 +104,7 @@ function ModelDeploymentCreate() {
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
form.setFieldValue(name, res);
form.setFieldValue(name, res.path);
} else { } else {
const response = res as ResourceSelectorResponse; const response = res as ResourceSelectorResponse;
const showValue = `${response.name}:${response.version}`; const showValue = `${response.name}:${response.version}`;


+ 17
- 28
react-ui/src/pages/ModelDeployment/Info/index.less View File

@@ -1,6 +1,16 @@
.model-deployment-info { .model-deployment-info {
height: 100%; height: 100%;


&__content {
display: flex;
flex-direction: column;
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 0;
background-color: white;
border-radius: 10px;
}

&__basic { &__basic {
&__item { &__item {
display: flex; display: flex;
@@ -23,34 +33,13 @@
} }
} }


&__content {
height: calc(100% - 60px);
&__guide {
flex: 1;
margin-top: 10px; margin-top: 10px;
padding: 30px 30px 0;
background-color: white;
border-radius: 10px;

&__title {
display: flex;
align-items: center;
}

&__table {
:global {
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
}
.ant-spin-container {
height: 100%;
}
.ant-table {
height: calc(100% - 74px);
overflow: auto;
}
}
}
}
padding: 10px;
overflow-y: auto;
color: white;
white-space: pre-wrap;
background-color: rgba(0, 0, 0, 0.85);
} }
} }

+ 145
- 121
react-ui/src/pages/ModelDeployment/Info/index.tsx View File

@@ -8,48 +8,70 @@ import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import { useSessionStorage } from '@/hooks/sessionStorage'; import { useSessionStorage } from '@/hooks/sessionStorage';
import { getModelDeploymentDocsReq } from '@/services/modelDeployment';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modelDeploymentInfoKey } from '@/utils/sessionStorage'; import { modelDeploymentInfoKey } from '@/utils/sessionStorage';
import { Col, Row, Tabs, type TabsProps } from 'antd'; import { Col, Row, Tabs, type TabsProps } from 'antd';
import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell'; import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell';
import { ModelDeploymentData } from '../types'; import { ModelDeploymentData } from '../types';
import styles from './index.less'; import styles from './index.less';


export enum ModelDeploymentTabKey {
Predict = 'Predict',
Guide = 'Guide',
Log = 'Log',
}

const tabItems = [ const tabItems = [
{ {
key: '1',
key: ModelDeploymentTabKey.Predict,
label: '预测', label: '预测',
icon: <KFIcon type="icon-yuce" />, icon: <KFIcon type="icon-yuce" />,
}, },
{ {
key: '2',
key: ModelDeploymentTabKey.Guide,
label: '调用指南', label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />, icon: <KFIcon type="icon-tiaoyongzhinan" />,
}, },
{ {
key: '3',
key: ModelDeploymentTabKey.Log,
label: '服务日志', label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />, icon: <KFIcon type="icon-fuwurizhi" />,
}, },
]; ];


function ModelDeploymentInfo() { function ModelDeploymentInfo() {
const [activeTab, setActiveTab] = useState<string>('1');
const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict);
const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>( const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>(
modelDeploymentInfoKey, modelDeploymentInfoKey,
true, true,
undefined, undefined,
); );
const getResourceDescription = useComputingResource()[2]; const getResourceDescription = useComputingResource()[2];
const [docs, setDocs] = useState('');


useEffect(() => {}, []);
useEffect(() => {
getModelDeploymentDocs();
}, [modelDeployementInfo]);

// 获取模型部署文档
const getModelDeploymentDocs = async () => {
const params = pick(modelDeployementInfo, ['service_id', 'service_ins_id']);
const [res] = await to(getModelDeploymentDocsReq(params));
if (res && res.data && res.data.docs) {
setDocs(JSON.stringify(res.data.docs, null, 2));
}
};


// 切换 Tab,重置数据 // 切换 Tab,重置数据
const hanleTabChange: TabsProps['onChange'] = (value) => { const hanleTabChange: TabsProps['onChange'] = (value) => {
setActiveTab(value); setActiveTab(value);
}; };


// 格式化环境变量
const formatEnvText = () => { const formatEnvText = () => {
if (!modelDeployementInfo?.env) { if (!modelDeployementInfo?.env) {
return '--'; return '--';
@@ -64,128 +86,130 @@ function ModelDeploymentInfo() {
<div className={styles['model-deployment-info']}> <div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle> <PageTitle title="服务详情"></PageTitle>
<div className={styles['model-deployment-info__content']}> <div className={styles['model-deployment-info__content']}>
<div>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['model-deployment-info__basic']}>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>服务名称:</div>
<div className={styles['value']}>
{modelDeployementInfo?.service_name ?? '--'}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>镜  像:</div>
<div className={styles['value']}>{modelDeployementInfo?.image ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>状  态:</div>
<div className={styles['value']}>
{ModelDeploymentStatusCell(modelDeployementInfo?.status)}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>模  型:</div>
<div className={styles['value']}>
{modelDeployementInfo?.model?.show_value ?? '--'}
</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建人:</div>
<div className={styles['value']}>{modelDeployementInfo?.created_by ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>挂载路径:</div>
<div className={styles['value']}>{modelDeployementInfo?.model_path ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>API URL:</div>
<div className={styles['value']}>{modelDeployementInfo?.url ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>副本数量:</div>
<div className={styles['value']}>{modelDeployementInfo?.replicas ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.create_time
? formatDate(modelDeployementInfo.create_time)
: '--'}
</div>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['model-deployment-info__basic']}>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>服务名称:</div>
<div className={styles['value']}>{modelDeployementInfo?.service_name ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>镜  像:</div>
<div className={styles['value']}>{modelDeployementInfo?.image ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>状  态:</div>
<div className={styles['value']}>
{ModelDeploymentStatusCell(modelDeployementInfo?.status)}
</div> </div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>更新时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.update_time
? formatDate(modelDeployementInfo.update_time)
: '--'}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>模  型:</div>
<div className={styles['value']}>
{modelDeployementInfo?.model?.show_value ?? '--'}
</div> </div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>环境变量:</div>
<div className={styles['value']}>{formatEnvText()}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建人:</div>
<div className={styles['value']}>{modelDeployementInfo?.created_by ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>挂载路径:</div>
<div className={styles['value']}>{modelDeployementInfo?.model_path ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>API URL:</div>
<div className={styles['value']}>{modelDeployementInfo?.url ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>副本数量:</div>
<div className={styles['value']}>{modelDeployementInfo?.replicas ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.create_time
? formatDate(modelDeployementInfo.create_time)
: '--'}
</div> </div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>资源规格:</div>
<div className={styles['value']}>
{modelDeployementInfo?.resource
? getResourceDescription(modelDeployementInfo.resource)
: '--'}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>更新时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.update_time
? formatDate(modelDeployementInfo.update_time)
: '--'}
</div> </div>
</Col>
</Row>
<Row gutter={40}>
<Col span={18}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>描  述:</div>
<div className={styles['value']}>{modelDeployementInfo?.description ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>环境变量:</div>
<div className={styles['value']}>{formatEnvText()}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>资源规格:</div>
<div className={styles['value']}>
{modelDeployementInfo?.resource
? getResourceDescription(modelDeployementInfo.resource)
: '--'}
</div> </div>
</Col>
</Row>
</div>
<div style={{ marginTop: '20px' }}>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</div>
</div>
</Col>
</Row>
<Row gutter={40}>
<Col span={18}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>描  述:</div>
<div className={styles['value']}>{modelDeployementInfo?.description ?? '--'}</div>
</div>
</Col>
</Row>
</div> </div>
<Tabs
activeKey={activeTab}
style={{ marginTop: '20px' }}
items={tabItems}
onChange={hanleTabChange}
/>
{activeTab === ModelDeploymentTabKey.Guide && (
<div className={styles['model-deployment-info__guide']}>{docs}</div>
)}
</div> </div>
</div> </div>
); );


react-ui/src/pages/Pipeline/editPipeline/modelMenus.less → react-ui/src/pages/Pipeline/components/ModelMenu/index.less View File

@@ -1,4 +1,5 @@
.collapse { .collapse {
flex: none;
width: 250px; width: 250px;
height: 100%; height: 100%;


@@ -35,14 +36,15 @@
align-items: center; align-items: center;
height: 40px; height: 40px;
padding: 0 16px; padding: 0 16px;
color: #575757;
color: @text-color-secondary;
font-size: 14px; font-size: 14px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
}
.collapseItem:hover {
color: #1664ff;
background: rgba(22, 100, 255, 0.08);

&:hover {
color: @primary-color;
background: rgba(22, 100, 255, 0.08);
}
} }
.modelMenusTitle { .modelMenusTitle {
margin-bottom: 10px; margin-bottom: 10px;

+ 91
- 0
react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx View File

@@ -0,0 +1,91 @@
import { getComponentAll } from '@/services/pipeline/index.js';
import { PipelineNodeModel } from '@/types';
import { to } from '@/utils/promise';
import { Collapse, type CollapseProps } from 'antd';
import { useEffect, useState } from 'react';
import Styles from './index.less';

type ModelMenuData = {
key: string;
name: string;
value: PipelineNodeModel[];
};

type ModelMenuProps = {
onComponentDragEnd: (
data: PipelineNodeModel & { x: number; y: number; label: string; img: string },
) => void;
};
const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => {
const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]);
const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]);

useEffect(() => {
getAllComponents();
}, []);

// 获取所有组件
const getAllComponents = async () => {
const [res] = await to(getComponentAll());
if (res && res.data) {
const menus = res.data as ModelMenuData[];
setModelMenusList(menus);
const items = menus.map((item) => {
return {
key: item.key,
label: item.name,
children: item.value.map((ele) => {
return (
<div
key={ele.id}
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
{ele.icon_path && (
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
alt=""
/>
)}
{ele.component_label}
</div>
);
}),
};
});
setCollapseItems(items);
}
};

const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => {
onComponentDragEnd({
...data,
x: e.clientX,
y: e.clientY,
label: data.component_label,
img: `/assets/images/${data.icon_path}.png`,
});
};

const defaultActiveKey = modelMenusList.map((item) => item.key + '');
return (
<div className={Styles.collapse}>
<div className={Styles.modelMenusTitle}>组件库</div>
{/* 这样 defaultActiveKey 才能生效 */}
{modelMenusList.length > 0 ? (
<Collapse
collapsible="header"
expandIconPosition="end"
defaultActiveKey={defaultActiveKey}
items={collapseItems}
></Collapse>
) : null}
</div>
);
};

export default ModelMenu;

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

@@ -20,9 +20,9 @@ export enum ResourceSelectorType {
} }


export type MirrorVersion = { export type MirrorVersion = {
id: number; // 镜像版本id
id: number; // 镜像版本 id
status: MirrorVersionStatus; // 镜像版本状态 status: MirrorVersionStatus; // 镜像版本状态
tag_name: string; // 镜像版本
tag_name: string; // 镜像版本 name
url: string; // 镜像版本路径 url: string; // 镜像版本路径
}; };


@@ -39,12 +39,13 @@ export type SelectorTypeInfo = {
tabItems: TabsProps['items']; tabItems: TabsProps['items'];
}; };


// 获取镜像列表,为了兼容数据集和模型
// 获取镜像文件列表,为了兼容数据集和模型
const getMirrorFilesReq = ({ id, version }: { id: number; version: string }): Promise<any> => { const getMirrorFilesReq = ({ id, version }: { id: number; version: string }): Promise<any> => {
const index = version.indexOf('-'); const index = version.indexOf('-');
const url = version.slice(index + 1); const url = version.slice(index + 1);
return Promise.resolve({ return Promise.resolve({
data: { data: {
path: url,
content: [ content: [
{ {
id: `${id}-${version}`, id: `${id}-${version}`,


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

@@ -15,26 +15,18 @@ import { MirrorVersion, ResourceSelectorType, selectorTypeConfig } from './confi
import styles from './index.less'; import styles from './index.less';
export { ResourceSelectorType, selectorTypeConfig }; export { ResourceSelectorType, selectorTypeConfig };


// 选择数据集和模型的返回类型
// 选择数据集\模型\镜像的返回类型
export type ResourceSelectorResponse = { export type ResourceSelectorResponse = {
id: number; // 数据集或者模型 id
name: string; // 数据集或者模型 name
version: string; // 数据集或者模型版本
path: string; // 数据集或者模型版本路径
id: number; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
version: string; // 数据集\模型\镜像版本
path: string; // 数据集\模型\镜像版本路径
activeTab: CommonTabKeys; // 是我的还是公开的 activeTab: CommonTabKeys; // 是我的还是公开的
}; };


export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 模型 | 数据集
defaultExpandedKeys?: React.Key[];
defaultCheckedKeys?: React.Key[];
defaultActiveTab?: CommonTabKeys;
onOk?: (params: ResourceSelectorResponse | string | null) => void;
}

type ResourceGroup = { type ResourceGroup = {
id: number; // 数据集或者模型 id
name: string; // 数据集或者模型 id
id: number; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
}; };


type ResourceFile = { type ResourceFile = {
@@ -42,9 +34,17 @@ type ResourceFile = {
file_name: string; // 文件 name file_name: string; // 文件 name
}; };


export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 数据集\模型\镜像
defaultExpandedKeys?: React.Key[];
defaultCheckedKeys?: React.Key[];
defaultActiveTab?: CommonTabKeys;
onOk?: (params: ResourceSelectorResponse | null) => void;
}

type TreeRef = GetRef<typeof Tree<TreeDataNode>>; type TreeRef = GetRef<typeof Tree<TreeDataNode>>;


// list 转成 treeData
// list 数据转成 treeData
const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => { const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => {
return list.map((v) => ({ return list.map((v) => ({
title: v.name, title: v.name,
@@ -54,7 +54,7 @@ const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => {
})); }));
}; };


// 版本转成 treeData
// 版本数据转成 treeData
const convertVersionToTreeData = (parentId: number) => { const convertVersionToTreeData = (parentId: number) => {
return (item: string | MirrorVersion): TreeDataNode => { return (item: string | MirrorVersion): TreeDataNode => {
if (typeof item === 'string') { if (typeof item === 'string') {
@@ -88,7 +88,7 @@ const updateChildren = (parentId: number, children: TreeDataNode[]) => {
}; };
}; };


// 得到数据集或者模型 id 和下属版本号
// 得到数据集\模型\镜像 id 和下属版本号
const getIdAndVersion = (versionKey: string) => { const getIdAndVersion = (versionKey: string) => {
const index = versionKey.indexOf('-'); const index = versionKey.indexOf('-');
const id = Number(versionKey.slice(0, index)); const id = Number(versionKey.slice(0, index));
@@ -137,12 +137,12 @@ function ResourceSelectorModal({
[originTreeData, searchText], [originTreeData, searchText],
); );


// 获取数据集或模型列表
// 获取数据集\模型\镜像列表
const getTreeData = async () => { const getTreeData = async () => {
const available_range = activeTab === CommonTabKeys.Private ? 0 : 1; const available_range = activeTab === CommonTabKeys.Private ? 0 : 1;
const params = { const params = {
page: 0, page: 0,
size: 200,
size: 1000,
[selectorTypeConfig[type].litReqParamKey]: available_range, [selectorTypeConfig[type].litReqParamKey]: available_range,
}; };
const getListReq = selectorTypeConfig[type].getList; const getListReq = selectorTypeConfig[type].getList;
@@ -159,7 +159,7 @@ function ResourceSelectorModal({
} }
}; };


// 获取数据集或模型版本列表
// 获取数据集\模型\镜像版本列表
const getVersions = async (parentId: number) => { const getVersions = async (parentId: number) => {
const getVersionsReq = selectorTypeConfig[type].getVersions; const getVersionsReq = selectorTypeConfig[type].getVersions;
const [res, error] = await to(getVersionsReq(parentId)); const [res, error] = await to(getVersionsReq(parentId));
@@ -266,21 +266,17 @@ function ResourceSelectorModal({
// 提交 // 提交
const handleOk = () => { const handleOk = () => {
if (checkedKeys.length > 0) { if (checkedKeys.length > 0) {
if (type === ResourceSelectorType.Mirror) {
onOk?.(files[0].file_name);
} else {
const last = checkedKeys[0] as string;
const { id, version } = getIdAndVersion(last);
const name = (treeData.find((v) => Number(v.key) === id)?.title ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
activeTab: activeTab as CommonTabKeys,
};
onOk?.(res);
}
const last = checkedKeys[0] as string;
const { id, version } = getIdAndVersion(last);
const name = (treeData.find((v) => Number(v.key) === id)?.title ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
activeTab: activeTab as CommonTabKeys,
};
onOk?.(res);
} else { } else {
onOk?.(null); onOk?.(null);
} }


+ 79
- 146
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -1,6 +1,8 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { useStateRef, useVisible } from '@/hooks'; import { useStateRef, useVisible } from '@/hooks';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { App, Button } from 'antd'; import { App, Button } from 'antd';
@@ -8,8 +10,8 @@ import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { s8 } from '../../../utils'; import { s8 } from '../../../utils';
import GlobalParamsDrawer from '../components/GlobalParamsDrawer'; import GlobalParamsDrawer from '../components/GlobalParamsDrawer';
import ModelMenu from '../components/ModelMenu';
import styles from './index.less'; import styles from './index.less';
import ModelMenus from './modelMenus';
import Props from './props'; import Props from './props';
import { findAllParentNodes, findFirstDuplicate } from './utils'; import { findAllParentNodes, findFirstDuplicate } from './utils';


@@ -27,6 +29,11 @@ const EditPipeline = () => {
const { message } = App.useApp(); const { message } = App.useApp();
let sourceAnchorIdx, targetAnchorIdx; let sourceAnchorIdx, targetAnchorIdx;


useEffect(() => {
initMenu();
getFirstWorkflow(locationParams.id);
}, []);

const onDragEnd = (val) => { const onDragEnd = (val) => {
console.log(val); console.log(val);
const _x = val.x; const _x = val.x;
@@ -51,8 +58,16 @@ const EditPipeline = () => {
return item.id === val.id; return item.id === val.id;
}); });
data.nodes[index] = val; data.nodes[index] = val;
const zoom = graph.getZoom();
// 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置
const lastPoint = graph.getCanvasByPoint(0, 0);
graph.changeData(data); graph.changeData(data);
graph.render(); graph.render();
graph.zoomTo(zoom);
// 获取重新渲染之后点(0, 0)在画布的位置
const newPoint = graph.getCanvasByPoint(0, 0);
// 移动画布相对位移;
graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y);
} }
}; };
const savePipeline = async (val) => { const savePipeline = async (val) => {
@@ -89,26 +104,14 @@ const EditPipeline = () => {
closeParamsDrawer(); closeParamsDrawer();
setTimeout(() => { setTimeout(() => {
if (val) { if (val) {
navgite({ pathname: `/pipeline` });
navgite({ pathname: `/pipeline/template` });
} }
}, 500); }, 500);
}); });
}, 500); }, 500);
}; };
const handlerClick = (e) => {
e.stopPropagation();
if (e.target.get('name') !== 'anchor-point' && e.item) {
graph.setItemState(e.item, 'nodeClicked', true);
const parentNodes = findAllParentNodes(graph, e.item);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
propsRef.current.showDrawer(e, globalParams, parentNodes);
}
};
const getGraphData = (data) => { const getGraphData = (data) => {
if (graph) { if (graph) {
console.log(data);
graph.data(data); graph.data(data);
graph.render(); graph.render();
} else { } else {
@@ -304,49 +307,8 @@ const EditPipeline = () => {


initGraph(); initGraph();
}; };
useEffect(() => {
initMenu();
getFirstWorkflow(locationParams.id);


return () => {
graph.off('node:mouseenter', (e) => {
graph.setItemState(e.item, 'showAnchors', true);
graph.setItemState(e.item, 'nodeSelected', true);
});
graph.off('node:mouseleave', (e) => {
// this.graph.setItemState(e.item, 'showAnchors', false);
graph.setItemState(e.item, 'nodeSelected', false);
});
// graph.off('dblclick', handlerClick);
};
}, []);
const initGraph = () => { const initGraph = () => {
const fittingString = (str, maxWidth, fontSize) => {
const ellipsis = '...';
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`;
}
});
return res;
};
// 获取文本的长度
const getTextSize = (str, maxWidth, fontSize) => {
let width = G6.Util.getTextSize(str, fontSize)[0];
return width > maxWidth ? maxWidth : width;
};
G6.registerNode( G6.registerNode(
'rect-node', 'rect-node',
{ {
@@ -399,6 +361,7 @@ const EditPipeline = () => {
y: bbox.y + bbox.height * anchorPos[1], y: bbox.y + bbox.height * anchorPos[1],
fill: '#fff', fill: '#fff',
stroke: '#a4a4a5', stroke: '#a4a4a5',
cursor: 'crosshair',
}, },
name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point') name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
anchorPointIdx: i, // flag the idx of the anchor-point circle anchorPointIdx: i, // flag the idx of the anchor-point circle
@@ -412,14 +375,30 @@ const EditPipeline = () => {


// response the state changes and show/hide the link-point circles // response the state changes and show/hide the link-point circles
setState(name, value, item) { setState(name, value, item) {
const anchorPoints = item
.getContainer()
.findAll((ele) => ele.get('name') === 'anchor-point');
anchorPoints.forEach((point) => {
if (value || point.get('links') > 0) point.show();
else point.hide();
});
// }
// const 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((ele) => ele.get('name') === 'anchor-point');
if (name === 'hover') {
if (value) {
shape.attr('stroke', themes['primaryColor']);
anchorPoints.forEach((point) => {
point.show();
});
} else {
shape.attr('stroke', '#fff');
anchorPoints.forEach((point) => {
point.hide();
});
}
}
}, },
}, },
'rect', 'rect',
@@ -427,12 +406,11 @@ const EditPipeline = () => {


graph = new G6.Graph({ graph = new G6.Graph({
container: graphRef.current, container: graphRef.current,
grid: true,
width: graphRef.current.clientWidth || 500, width: graphRef.current.clientWidth || 500,
height: graphRef.current.clientHeight || '100%', height: graphRef.current.clientHeight || '100%',
animate: false, animate: false,
groupByTypes: false, groupByTypes: false,
fitView: false,
fitView: true,
plugins: [contextMenu], plugins: [contextMenu],
enabledStack: true, enabledStack: true,
modes: { modes: {
@@ -511,20 +489,8 @@ const EditPipeline = () => {
lineWidth: 0.5, lineWidth: 0.5,
}, },
}, },
nodeStateStyles: {
nodeSelected: {
fill: 'red',
shadowColor: 'red',
stroke: 'red',
'text-shape': {
fill: 'red',
stroke: 'red',
},
},
},
defaultEdge: { defaultEdge: {
// type: 'quadratic',
// type: 'cubic-vertical',
//type: 'cubic-vertical',


style: { style: {
endArrow: { endArrow: {
@@ -567,17 +533,20 @@ const EditPipeline = () => {
// linkCenter: true, // linkCenter: true,
fitView: true, fitView: true,
minZoom: 0.5, minZoom: 0.5,
maxZoom: 3,
fitViewPadding: [320, 320, 220, 320],
maxZoom: 5,
fitViewPadding: 300,
});
graph.on('node:click', (e) => {
e.stopPropagation();
if (e.target.get('name') !== 'anchor-point' && e.item) {
// graph.setItemState(e.item, 'nodeClicked', true);
const parentNodes = findAllParentNodes(graph, e.item);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
propsRef.current.showDrawer(e, globalParams, parentNodes);
}
}); });
// graph.on('dblclick', (e) => {
// console.log(e.item);
// if (e.item) {
// graph.setItemState(e.item, 'nodeClicked', true);
// handlerClick(e);
// }
// });
graph.on('node:click', handlerClick);
graph.on('aftercreateedge', (e) => { graph.on('aftercreateedge', (e) => {
// update the sourceAnchor and targetAnchor for the newly added edge // update the sourceAnchor and targetAnchor for the newly added edge
graph.updateItem(e.edge, { graph.updateItem(e.edge, {
@@ -595,59 +564,6 @@ const EditPipeline = () => {
}); });
}); });
}); });
graph.on('node:mouseenter', (e) => {
// this.graph.setItemState(e.item, 'showAnchors', true);
graph.setItemState(e.item, 'nodeSelected', true);
graph.updateItem(e.item, {
// 节点的样式
style: {
stroke: '#1664ff',
},
});
});
graph.on('node:mouseleave', (e) => {
// this.graph.setItemState(e.item, 'showAnchors', false);
graph.setItemState(e.item, 'nodeSelected', false);
graph.updateItem(e.item, {
// 节点的样式
style: {
stroke: 'transparent',
},
});
});
graph.on('node:dragenter', (e) => {
console.log(e.target.get('name'));
console.log('node:dragenter');
graph.setItemState(e.item, 'nodeSelected', true);
graph.updateItem(e.item, {
// 节点的样式
style: {
stroke: '#1664ff',
},
});
});
graph.on('node:dragleave', (e) => {
console.log(e.target.get('name'));
console.log('node:dragleave');
graph.setItemState(e.item, 'nodeSelected', false);
graph.updateItem(e.item, {
// 节点的样式
style: {
stroke: 'transparent',
},
});
});
graph.on('node:dragstart', (e) => {
console.log('node:dragstart');
graph.setItemState(e.item, 'nodeSelected', true);
graph.updateItem(e.item, {
// 节点的样式
style: {
stroke: '#1664ff',
},
});
});

graph.on('afterremoveitem', (e) => { graph.on('afterremoveitem', (e) => {
if (e.item && e.item.source && e.item.target) { if (e.item && e.item.source && e.item.target) {
const sourceNode = graph.findById(e.item.source); const sourceNode = graph.findById(e.item.source);
@@ -673,7 +589,6 @@ const EditPipeline = () => {
} }
} }
}); });

// after clicking on the first node, the edge is created, update the sourceAnchor // after clicking on the first node, the edge is created, update the sourceAnchor
graph.on('afteradditem', (e) => { graph.on('afteradditem', (e) => {
if (e.item && e.item.getType() === 'edge') { if (e.item && e.item.getType() === 'edge') {
@@ -682,16 +597,34 @@ const EditPipeline = () => {
}); });
} }
}); });
graph.on('node:mouseenter', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
graph.on('node:dragenter', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:dragleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
graph.on('node:dragstart', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:drag', (e) => {
graph.setItemState(e.item, 'hover', true);
});
window.onresize = () => { window.onresize = () => {
if (!graph || graph.get('destroyed')) return; if (!graph || graph.get('destroyed')) return;
if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight)
return;
graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20);
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
}; };
}; };
return ( return (
<div className={styles['pipeline-container']}> <div className={styles['pipeline-container']}>
<ModelMenus onParDragEnd={onDragEnd}></ModelMenus>
<ModelMenu onComponentDragEnd={onDragEnd}></ModelMenu>
<div className={styles['pipeline-container__workflow']}> <div className={styles['pipeline-container__workflow']}>
<div className={styles['pipeline-container__workflow__top']}> <div className={styles['pipeline-container__workflow__top']}>
<Button <Button


+ 5
- 4
react-ui/src/pages/Pipeline/editPipeline/index.less View File

@@ -4,7 +4,8 @@
background-color: #fff; background-color: #fff;


&__workflow { &__workflow {
flex: 1;
flex: 1 1 0;
min-width: 0;
height: 100%; height: 100%;


&__top { &__top {
@@ -12,15 +13,15 @@
align-items: center; align-items: center;
justify-content: end; justify-content: end;
width: 100%; width: 100%;
height: 45px;
padding: 0 30px;
height: 52px;
padding: 0 20px;
background: #ffffff; background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
} }


&__graph { &__graph {
width: 100%; width: 100%;
height: calc(100% - 45px);
height: calc(100% - 52px);
background-color: @background-color; background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png); background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%; background-size: 100% 100%;


+ 0
- 67
react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx View File

@@ -1,67 +0,0 @@
import { getComponentAll } from '@/services/pipeline/index.js';
import { Collapse } from 'antd';
import { useEffect, useState } from 'react';
import Styles from './modelMenus.less';
const ModelMenus = ({ onParDragEnd }) => {
const [modelMenusList, setModelMenusList] = useState([]);
useEffect(() => {
getComponentAll().then((ret) => {
console.log(ret);
if (ret.code === 200) {
setModelMenusList(ret.data);
}
});
}, []);
const dragEnd = (e, data) => {
console.log(e, data);
onParDragEnd({
...data,
x: e.clientX,
y: e.clientY,
label: data.component_label,
img: `/assets/images/${data.icon_path}.png`,
});
};
const { Panel } = Collapse;
return (
<div className={Styles.collapse}>
<div className={Styles.modelMenusTitle}>组件库</div>
{modelMenusList && modelMenusList.length > 0 ? (
<Collapse
collapsible="header"
defaultActiveKey={modelMenusList.map((item) => item.key + '')}
expandIconPosition="end"
>
{modelMenusList && modelMenusList.length > 0
? modelMenusList.map((item) => (
<Panel header={<div>{item.name}</div>} key={item.key}>
{item.value && item.value.length > 0
? item.value.map((ele) => (
<div
key={ele.id}
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
{ele.icon_path && (
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
alt=""
/>
)}
{ele.component_label}
</div>
))
: ''}
</Panel>
))
: ''}
</Collapse>
) : null}
</div>
);
};
export default ModelMenus;

react-ui/src/pages/Pipeline/editPipeline/props.jsx → react-ui/src/pages/Pipeline/editPipeline/props.tsx View File

@@ -1,11 +1,19 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import ParameterInput from '@/components/ParameterInput'; import ParameterInput from '@/components/ParameterInput';
import ParameterSelect from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import {
PipelineGlobalParam,
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Button, Drawer, Form, Input, Select } from 'antd';
import { pick } from 'lodash';
import { INode } from '@antv/g6';
import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import PropsLabel from '../components/PropsLabel'; import PropsLabel from '../components/PropsLabel';
import ResourceSelectorModal, { import ResourceSelectorModal, {
@@ -16,18 +24,22 @@ import styles from './props.less';
import { canInput, createMenuItems } from './utils'; import { canInput, createMenuItems } from './utils';
const { TextArea } = Input; const { TextArea } = Input;


const Props = forwardRef(({ onParentChange }, ref) => {
type PipelineNodeParameterProps = {
onParentChange: (data: PipelineNodeModelSerialize) => void;
};

const PipelineNodeParameter = forwardRef(({ onParentChange }: PipelineNodeParameterProps, ref) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState({});
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize,
);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState(undefined); // 选择的模型,为了再次打开时恢复原来的选择
const [selectedDataset, setSelectedDataset] = useState(undefined); // 选择的数据集,为了再次打开时恢复原来的选择
const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模
const [menuItems, setMenuItems] = useState([]);
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);


const afterOpenChange = () => { const afterOpenChange = () => {
if (!open) { if (!open) {
console.log('zzzzz', form.getFieldsValue());
console.log('getFieldsValue', form.getFieldsValue());
const control_strategy = form.getFieldValue('control_strategy'); const control_strategy = form.getFieldValue('control_strategy');
const in_parameters = form.getFieldValue('in_parameters'); const in_parameters = form.getFieldValue('in_parameters');
const out_parameters = form.getFieldValue('out_parameters'); const out_parameters = form.getFieldValue('out_parameters');
@@ -54,7 +66,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
return Promise.reject(propsError); return Promise.reject(propsError);
} }
}, },
showDrawer(e, params, parentNodes) {
showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) {
if (e.item && e.item.getModel()) { if (e.item && e.item.getModel()) {
form.resetFields(); form.resetFields();
const model = e.item.getModel(); const model = e.item.getModel();
@@ -75,8 +87,6 @@ const Props = forwardRef(({ onParentChange }, ref) => {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
setSelectedModel(undefined);
setSelectedDataset(undefined);
setOpen(true); setOpen(true);


// 参数下拉菜单 // 参数下拉菜单
@@ -89,50 +99,70 @@ const Props = forwardRef(({ onParentChange }, ref) => {
})); }));


// 选择数据集、模型、镜像 // 选择数据集、模型、镜像
const selectResource = (name, item) => {
let type;
let resource;
const selectResource = (
formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => {
let type: ResourceSelectorType;
switch (item.item_type) { switch (item.item_type) {
case 'dataset': case 'dataset':
type = ResourceSelectorType.Dataset; type = ResourceSelectorType.Dataset;
resource = selectedDataset;
break; break;
case 'model': case 'model':
type = ResourceSelectorType.Model; type = ResourceSelectorType.Model;
resource = selectedModel;
break; break;
default: default:
type = ResourceSelectorType.Mirror; type = ResourceSelectorType.Mirror;
break; break;
} }

const fieldValue = form.getFieldValue(formItemName);
const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined;
const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : [];
const checkedKeys = Array.isArray(fieldValue?.checkedKeys) ? fieldValue?.checkedKeys : [];
const { close } = openAntdModal(ResourceSelectorModal, { const { close } = openAntdModal(ResourceSelectorModal, {
type, type,
defaultExpandedKeys: resource ? [resource.id] : [],
defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
defaultActiveTab: resource?.activeTab,
defaultExpandedKeys: expandedKeys,
defaultCheckedKeys: checkedKeys,
defaultActiveTab: activeTab,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
form.setFieldValue(name, res);
const { activeTab, id, version, path } = res;
if (formItemName === 'image') {
form.setFieldValue(formItemName, path);
} else {
form.setFieldValue(formItemName, {
...item,
value: path,
showValue: path,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
}
} else { } else {
const jsonObj = pick(res, ['id', 'version', 'path']);
const { activeTab, id, name, version, path } = res;
const jsonObj = {
id,
version,
path,
};
const value = JSON.stringify(jsonObj); const value = JSON.stringify(jsonObj);
const showValue = `${res.name}:${res.version}`;
form.setFieldValue(name, { ...item, value, showValue, fromSelect: true });

if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(res);
} else if (type === ResourceSelectorType.Model) {
setSelectedModel(res);
}
const showValue = `${name}:${version}`;
form.setFieldValue(formItemName, {
...item,
value,
showValue,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
} }
} else { } else {
if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(undefined);
} else if (type === ResourceSelectorType.Model) {
setSelectedModel(undefined);
}
form.setFieldValue(name, '');
form.setFieldValue(formItemName, '');
} }
close(); close();
}, },
@@ -140,9 +170,9 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}; };


// 获取选择数据集、模型后面按钮 icon // 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item) => {
const getSelectBtnIcon = (item: { item_type: string }) => {
const type = item.item_type; const type = item.item_type;
let selectorType;
let selectorType: ResourceSelectorType;
if (type === 'dataset') { if (type === 'dataset') {
selectorType = ResourceSelectorType.Dataset; selectorType = ResourceSelectorType.Dataset;
} else if (type === 'model') { } else if (type === 'model') {
@@ -155,10 +185,33 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}; };


// 参数回填 // 参数回填
const handleParameterClick = (name, value) => {
const handleParameterClick = (name: NamePath, value: any) => {
form.setFieldValue(name, value); form.setFieldValue(name, value);
}; };


// form item label
const getLabel = (
item: { key: string; value: PipelineNodeModelParameter },
namePrefix: string,
) => {
return item.value.type === 'select' ? (
item.value.label + '(' + item.key + ')'
) : (
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
);
};

// 控制策略 // 控制策略
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
([key, value]) => ({ key, value }), ([key, value]) => ({ key, value }),
@@ -290,7 +343,6 @@ const Props = forwardRef(({ onParentChange }, ref) => {
]} ]}
> >
<Select <Select
showSearch
placeholder="请选择资源规格" placeholder="请选择资源规格"
filterOption={filterResourceStandard} filterOption={filterResourceStandard}
options={resourceStandardList} options={resourceStandardList}
@@ -298,6 +350,8 @@ const Props = forwardRef(({ onParentChange }, ref) => {
label: 'description', label: 'description',
value: 'standard', value: 'standard',
}} }}
showSearch
allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -332,31 +386,9 @@ const Props = forwardRef(({ onParentChange }, ref) => {
<Form.Item <Form.Item
key={item.key} key={item.key}
name={['control_strategy', item.key]} name={['control_strategy', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label}
onClick={(value) => {
handleParameterClick(['control_strategy', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
// getValueProps={(e) => {
// return { value: e.value };
// }}
// getValueFromEvent={(e) => {
// return {
// ...item.value,
// value: e.target.value,
// };
// }}
label={getLabel(item, 'control_strategy')}
> >
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
<ParameterInput allowClear></ParameterInput>
</Form.Item> </Form.Item>
))} ))}
<div className={styles['pipeline-drawer__title']}> <div className={styles['pipeline-drawer__title']}>
@@ -365,20 +397,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
{inParametersList.map((item) => ( {inParametersList.map((item) => (
<Form.Item <Form.Item
key={item.key} key={item.key}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick(['in_parameters', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
label={getLabel(item, 'in_parameters')}
required={item.value.require ? true : false} required={item.value.require ? true : false}
> >
<div className={styles['pipeline-drawer__ref-row']}> <div className={styles['pipeline-drawer__ref-row']}>
@@ -387,11 +406,11 @@ const Props = forwardRef(({ onParentChange }, ref) => {
noStyle noStyle
rules={[{ required: item.value.require ? true : false }]} rules={[{ required: item.value.require ? true : false }]}
> >
<ParameterInput
placeholder={item.value.placeholder}
canInput={canInput(item.value)}
allowClear
></ParameterInput>
{item.value.type === 'select' ? (
<ParameterSelect />
) : (
<ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput>
)}
</Form.Item> </Form.Item>
{item.value.type === 'ref' && ( {item.value.type === 'ref' && (
<Form.Item noStyle> <Form.Item noStyle>
@@ -416,32 +435,10 @@ const Props = forwardRef(({ onParentChange }, ref) => {
<Form.Item <Form.Item
key={item.key} key={item.key}
name={['out_parameters', item.key]} name={['out_parameters', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick(['out_parameters', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
label={getLabel(item, 'out_parameters')}
rules={[{ required: item.value.require ? true : false }]} rules={[{ required: item.value.require ? true : false }]}
// getValueProps={(e) => {
// return { value: e.value };
// }}
// getValueFromEvent={(e) => {
// return {
// ...item.value,
// value: e.target.value,
// };
// }}
> >
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
<ParameterInput allowClear></ParameterInput>
</Form.Item> </Form.Item>
))} ))}
</Form> </Form>
@@ -449,4 +446,4 @@ const Props = forwardRef(({ onParentChange }, ref) => {
); );
}); });


export default Props;
export default PipelineNodeParameter;

+ 1
- 0
react-ui/src/pages/Pipeline/editPipeline/utils.tsx View File

@@ -77,6 +77,7 @@ export function getInParameterComponent(
return null; return null;
} }


// 判断是否允许输入
export function canInput(parameter: PipelineNodeModelParameter) { export function canInput(parameter: PipelineNodeModelParameter) {
const { type, item_type } = parameter; const { type, item_type } = parameter;
return !( return !(


+ 1
- 2
react-ui/src/pages/Pipeline/index.jsx View File

@@ -101,9 +101,8 @@ const Pipeline = () => {
page: pageOption.current.page - 1, page: pageOption.current.page - 1,
size: pageOption.current.size, size: pageOption.current.size,
}; };
console.log(params, pageOption);
getWorkflow(params).then((ret) => { getWorkflow(params).then((ret) => {
if (ret.code == 200) {
if (ret.code === 200) {
setPipeList(ret.data.content); setPipeList(ret.data.content);


setTotal(ret.data.totalElements); setTotal(ret.data.totalElements);


+ 1
- 1
react-ui/src/pages/Pipeline/index.less View File

@@ -13,7 +13,7 @@
} }


.PipelineBox { .PipelineBox {
height: calc(100% - 20px);
height: 100%;
.PipelineTable { .PipelineTable {
height: calc(100% - 60px); height: calc(100% - 60px);
:global { :global {


+ 2
- 2
react-ui/src/pages/Workspace/components/QuickStart/index.tsx View File

@@ -148,14 +148,14 @@ function QuickStart() {
x={left + 2 * (192 + space) + 56} x={left + 2 * (192 + space) + 56}
y={139} y={139}
width={taskLeftArrowWidth} width={taskLeftArrowWidth}
height={125}
height={120}
arrowLeft={taskLeftArrowWidth} arrowLeft={taskLeftArrowWidth}
arrorwTop={-4} arrorwTop={-4}
borderLeft={1} borderLeft={1}
borderTop={1} borderTop={1}
/> />
<WorkArrow <WorkArrow
x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 6}
x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4}
y={127} y={127}
width={taskRightArrowWidth} width={taskRightArrowWidth}
height={156} height={156}


+ 1
- 0
react-ui/src/requestConfig.ts View File

@@ -42,6 +42,7 @@ export const requestConfig: RequestConfig = {
message.error('请重新登录'); message.error('请重新登录');
return Promise.reject(response); return Promise.reject(response);
} else { } else {
console.log(message, data);
message.error(data?.msg ?? '请求失败'); message.error(data?.msg ?? '请求失败');
return Promise.reject(response); return Promise.reject(response);
} }


+ 8
- 0
react-ui/src/services/dataset/index.js View File

@@ -130,3 +130,11 @@ export function deleteDataset(id) {
method: 'DELETE', method: 'DELETE',
}); });
} }

// 获取模型依赖
export function getModelAtlasReq(data) {
return request(`/api/mmp/modelDependency/queryModelAtlas`, {
method: 'POST',
data
});
}

+ 8
- 0
react-ui/src/services/modelDeployment/index.ts View File

@@ -59,3 +59,11 @@ export function updateModelDeploymentReq(data: any) {
data, data,
}); });
} }

// 获取模型部署操作指南
export function getModelDeploymentDocsReq(data: any) {
return request(`/api/v1/model/getDocs`, {
method: 'POST',
data,
});
}

+ 2
- 2
react-ui/src/styles/menu.less View File

@@ -4,7 +4,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
font-size: 16px;
font-size: @font-size-content;


.anticon.kf-menu-item__default-icon { .anticon.kf-menu-item__default-icon {
display: inline !important; display: inline !important;
@@ -54,6 +54,6 @@


.ant-menu-submenu { .ant-menu-submenu {
.ant-menu-submenu-title:hover { .ant-menu-submenu-title:hover {
color: #1664ff !important;
color: @primary-color !important;
} }
} }

+ 4
- 1
react-ui/src/styles/theme.less View File

@@ -11,10 +11,11 @@
@background-color: #f9fafb; // 页面背景颜色 @background-color: #f9fafb; // 页面背景颜色
@text-color: #1d1d20; @text-color: #1d1d20;
@text-color-secondary: #575757; @text-color-secondary: #575757;
@success-color: #1ace62;
@success-color: #6ac21d;
@error-color: #c73131; @error-color: #c73131;
@warning-color: #f98e1b; @warning-color: #f98e1b;
@abort-color: #8a8a8a; @abort-color: #8a8a8a;
@pending-color: #ecb934;


@border-color: rgba(22, 100, 255, 0.3); @border-color: rgba(22, 100, 255, 0.3);
@border-color-secondary: rgba(22, 100, 255, 0.1); @border-color-secondary: rgba(22, 100, 255, 0.1);
@@ -78,4 +79,6 @@
fontSizeInput: @font-size-input; fontSizeInput: @font-size-input;
fontSizeInputLg: @font-size-input-lg; fontSizeInputLg: @font-size-input-lg;
siderBGColor: @sider-background-color; siderBGColor: @sider-background-color;
abortColor: @abort-color;
pendingColor: @pending-color;
} }

+ 16
- 6
react-ui/src/types.ts View File

@@ -41,23 +41,33 @@ export type PipelineNodeModel = {
control_strategy: string; control_strategy: string;
in_parameters: string; in_parameters: string;
out_parameters: string; out_parameters: string;
component_label: string;
icon_path: string;
}; };


// 流水线
// 流水线节点模型数据
export type PipelineNodeModelParameter = { export type PipelineNodeModelParameter = {
label: string;
value: any;
require: number;
type: string; type: string;
item_type: string; item_type: string;
label: string;
value: any;
require?: number;
placeholder?: string; placeholder?: string;
describe?: string; describe?: string;
fromSelect?: boolean; fromSelect?: boolean;
showValue?: any; showValue?: any;
editable: number;
editable?: number;
activeTab?: string; // ResourceSelectorModal tab
expandedKeys?: string[]; // ResourceSelectorModal expandedKeys
checkedKeys?: string[]; // ResourceSelectorModal checkedKeys
}; };


// type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType }
// 修改属性类型
export type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType };

// export type PascalCaseType<T> = {
// [K in keyof T as `${Capitalize<string & K>}`]: T[K];
// }


// 序列化后的流水线节点 // 序列化后的流水线节点
export type PipelineNodeModelSerialize = Omit< export type PipelineNodeModelSerialize = Omit<


+ 1
- 1
react-ui/src/utils/downloadfile.ts View File

@@ -29,7 +29,7 @@ export function resolveBlob(res: any, mimeType: string) {
document.body.removeChild(aLink); document.body.removeChild(aLink);
} }


export function downLoadZip(url: string, params: any) {
export function downLoadZip(url: string, params?: any) {
request(url, { request(url, {
method: 'GET', method: 'GET',
params, params,


+ 93
- 6
react-ui/src/utils/index.ts View File

@@ -4,6 +4,8 @@
* @Description: 工具类 * @Description: 工具类
*/ */


import G6 from '@antv/g6';

// 生成 8 位随机数 // 生成 8 位随机数
export function s8() { export function s8() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
@@ -29,8 +31,22 @@ export function parseJsonText(text?: string | null): any | null {
} }
} }


// underscore-to-camelCase
// 判断是否为对象
function isPlainObject(value: any) {
if (value === null || typeof value !== 'object') return false;
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
if (proto.constructor && proto.constructor !== Object) return false;
proto = Object.getPrototypeOf(proto);
}
return true;
}

// underscore to camelCase
export function underscoreToCamelCase(obj: Record<string, any>) { export function underscoreToCamelCase(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {}; const newObj: Record<string, any> = {};
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
@@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
return $1.toUpperCase().replace('[-_]', '').replace('_', ''); return $1.toUpperCase().replace('[-_]', '').replace('_', '');
}); });
let value = obj[key]; let value = obj[key];
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
value = value.map((item) => underscoreToCamelCase(item));
} else if (isPlainObject(value)) {
value = underscoreToCamelCase(value); value = underscoreToCamelCase(value);
} }
newObj[newKey] = value; newObj[newKey] = value;
@@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
return newObj; return newObj;
} }


// camelCase-to-underscore
// camelCase to underscore
export function camelCaseToUnderscore(obj: Record<string, any>) { export function camelCaseToUnderscore(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {}; const newObj: Record<string, any> = {};
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
let value = obj[key]; let value = obj[key];
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
value = value.map((item) => camelCaseToUnderscore(item));
} else if (isPlainObject(value)) {
value = camelCaseToUnderscore(value); value = camelCaseToUnderscore(value);
} }
newObj[newKey] = value; newObj[newKey] = value;
@@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) {
return newObj; return newObj;
} }


// null undefined
// null to undefined
export function nullToUndefined(obj: Record<string, any>) { export function nullToUndefined(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {}; const newObj: Record<string, any> = {};
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
const value = obj[key]; const value = obj[key];
if (value === null) { if (value === null) {
newObj[key] = undefined; newObj[key] = undefined;
} else if (typeof value === 'object' && value !== null) {
} else if (Array.isArray(value)) {
newObj[key] = value.map((item) => nullToUndefined(item));
} else if (isPlainObject(value)) {
newObj[key] = nullToUndefined(value); newObj[key] = nullToUndefined(value);
} else { } else {
newObj[key] = value; newObj[key] = value;
@@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) {
} }
return newObj; return newObj;
} }

/**
* Changes the property names of an object based on a mapping provided.
*
* @param obj - The object whose property names need to be changed.
* @param mapping - The mapping of old property names to new property names.
* @return The object with the changed property names.
*/
export function changePropertyName(obj: Record<string, any>, mapping: Record<string, string>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
const newKey = mapping.hasOwnProperty(key) ? mapping[key] : key;
if (Array.isArray(value)) {
value = value.map((item) => changePropertyName(item, mapping));
} else if (isPlainObject(value)) {
value = changePropertyName(value, mapping);
}
newObj[newKey] = value;
}
}
return newObj;
}

/**
* 计算显示的字符串
* @param tr 要裁剪的字符串
* @param maxWidth 最大宽度
* @param fontSize 字体大小
* @return 处理后的字符串
*/
export const fittingString = (str: string, maxWidth: number, fontSize: number) => {
if (!str) {
return '';
}
const ellipsis = '...';
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substring(0, i)}${ellipsis}`;
}
});
return res;
};

+ 11
- 7
react-ui/src/utils/ui.tsx View File

@@ -4,6 +4,7 @@
* @Description: UI 公共方法 * @Description: UI 公共方法
*/ */
import { PageEnum } from '@/enums/pagesEnums'; import { PageEnum } from '@/enums/pagesEnums';
import { removeAllPageCacheState } from '@/hooks/pageCacheState';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { history } from '@umijs/max'; import { history } from '@umijs/max';
import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd';
@@ -49,17 +50,20 @@ export const getFileListFromEvent = (e: any) => {
}); });
}; };


// 去登录页面
/**
* 跳转到登录页
* @param toHome 是否跳转到首页
*/
export const gotoLoginPage = (toHome: boolean = true) => { export const gotoLoginPage = (toHome: boolean = true) => {
const { pathname, search } = window.location;
const { pathname, search } = location;
const urlParams = new URLSearchParams(); const urlParams = new URLSearchParams();
urlParams.append('redirect', pathname + search); urlParams.append('redirect', pathname + search);
const newSearch =
toHome && pathname !== PageEnum.LOGIN && pathname !== '/' ? '' : urlParams.toString();
console.log('pathname', pathname);
console.log('search', search);
if (window.location.pathname !== PageEnum.LOGIN) {
const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString();
// console.log('pathname', pathname);
// console.log('search', search);
if (pathname !== PageEnum.LOGIN) {
closeAllModals(); closeAllModals();
removeAllPageCacheState();
history.replace({ history.replace({
pathname: PageEnum.LOGIN, pathname: PageEnum.LOGIN,
search: newSearch, search: newSearch,


+ 90
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java View File

@@ -0,0 +1,90 @@
package com.ruoyi.platform.controller.devEnvironment;

import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.GenericsAjaxResult;
import com.ruoyi.platform.domain.DevEnvironment;
import com.ruoyi.platform.service.DevEnvironmentService;
import io.swagger.annotations.Api;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
* (DevEnvironment)表控制层
*
* @author Xidaray
* @since 2024-06-03 15:17:37
*/
@RestController
@RequestMapping("devEnvironment")
@Api("开发环境管理")
public class DevEnvironmentController extends BaseController {
/**
* 服务对象
*/
@Resource
private DevEnvironmentService devEnvironmentService;

/**
* 分页查询
*
* @param devEnvironment 筛选条件
* @param page 页数
* @param size 每页大小
* @return 查询结果
*/
@GetMapping
public GenericsAjaxResult<Page<DevEnvironment>> queryByPage(DevEnvironment devEnvironment, int page, int size) {
PageRequest pageRequest = PageRequest.of(page,size);
return genericsSuccess(this.devEnvironmentService.queryByPage(devEnvironment, pageRequest));
}

/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
public ResponseEntity<DevEnvironment> queryById(@PathVariable("id") Integer id) {
return ResponseEntity.ok(this.devEnvironmentService.queryById(id));
}

/**
* 新增数据
*
* @param devEnvironment 实体
* @return 新增结果
*/
@PostMapping
public ResponseEntity<DevEnvironment> add(@RequestBody DevEnvironment devEnvironment) {
return ResponseEntity.ok(this.devEnvironmentService.insert(devEnvironment));
}

/**
* 编辑数据
*
* @param devEnvironment 实体
* @return 编辑结果
*/
@PutMapping
public ResponseEntity<DevEnvironment> edit(@RequestBody DevEnvironment devEnvironment) {
return ResponseEntity.ok(this.devEnvironmentService.update(devEnvironment));
}

/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping("{id}")
public ResponseEntity<String> deleteById(@PathVariable("id") Integer id) {
return ResponseEntity.ok(this.devEnvironmentService.removeById(id));
}

}


+ 1
- 1
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java View File

@@ -91,7 +91,7 @@ public class ExperimentController extends BaseController {
*/ */
@PutMapping @PutMapping
@ApiOperation("编辑实验") @ApiOperation("编辑实验")
public GenericsAjaxResult<Experiment> edit(@RequestBody Experiment experiment) throws IOException {
public GenericsAjaxResult<Experiment> edit(@RequestBody Experiment experiment) throws Exception {
return genericsSuccess(this.experimentService.update(experiment)); return genericsSuccess(this.experimentService.update(experiment));
} }




+ 32
- 3
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java View File

@@ -4,11 +4,11 @@ import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.domain.GenericsAjaxResult; import com.ruoyi.common.core.web.domain.GenericsAjaxResult;
import com.ruoyi.platform.service.JupyterService; import com.ruoyi.platform.service.JupyterService;
import com.ruoyi.platform.vo.FrameLogPathVo;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.web.bind.annotation.*;


import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.File; import java.io.File;
@@ -28,6 +28,35 @@ public class JupyterController extends BaseController {
return genericsSuccess(jupyterService.getJupyterServiceUrl()); return genericsSuccess(jupyterService.getJupyterServiceUrl());
} }



/**
* 启动jupyter容器接口
*
* @param id 开发环境配置id
* @return url
*/
@PostMapping("/run/{id}")
@ApiOperation("根据开发环境id启动jupyter pod")
@ApiResponse
public GenericsAjaxResult<String> runJupyter(@PathVariable("id") Integer id) throws Exception {
return genericsSuccess(this.jupyterService.runJupyterService(id));
}


/**
* 停止jupyter容器接口
*
* @param id 开发环境配置id
* @return 操作结果
*/
@DeleteMapping("/stop/{id}")
@ApiOperation("根据开发环境id停止jupyter pod")
@ApiResponse
public GenericsAjaxResult<String> stopJupyter(@PathVariable("id") Integer id) throws Exception {
return genericsSuccess(this.jupyterService.stopJupyterService(id));
}


@GetMapping(value = "/upload") @GetMapping(value = "/upload")
public AjaxResult upload() throws Exception { public AjaxResult upload() throws Exception {
File file = new File("D://nexus-deploy.yaml"); File file = new File("D://nexus-deploy.yaml");


+ 115
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelDependencyController.java View File

@@ -0,0 +1,115 @@
package com.ruoyi.platform.controller.model;

import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.GenericsAjaxResult;
import com.ruoyi.platform.domain.ModelDependency;
import com.ruoyi.platform.domain.ModelsVersion;
import com.ruoyi.platform.service.ModelDependencyService;
import com.ruoyi.platform.vo.ModelDependcyTreeVo;
import io.swagger.annotations.ApiOperation;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;

/**
* (ModelDependency)表控制层
*
* @author Xidaray
* @since 2024-05-29 13:51:23
*/
@RestController
@RequestMapping("modelDependency")
public class ModelDependencyController extends BaseController {
/**
* 服务对象
*/
@Resource
private ModelDependencyService modelDependencyService;

/**
* 分页查询
*
* @param modelDependency 筛选条件
* @param page 分页对象
* @param size 分页对象
* @return 查询结果
*/
@GetMapping
@ApiOperation("分页查询")
public GenericsAjaxResult<Page<ModelDependency>> queryByPage(ModelDependency modelDependency, int page ,int size) {
PageRequest pageRequest = PageRequest.of(page,size);
return genericsSuccess(this.modelDependencyService.queryByPage(modelDependency, pageRequest));
}

/**
* 根据对象查询
*
* @param modelDependency 筛选条件
* @return 查询结果
*/
@GetMapping("/queryModelDependency")
@ApiOperation("根据对象查询")
public GenericsAjaxResult<List<ModelDependency>> queryByModelDependency(@RequestBody ModelDependency modelDependency) throws IOException {
modelDependency.setState(1);
return genericsSuccess(this.modelDependencyService.queryByModelDependency(modelDependency));
}

/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
@ApiOperation("根据id查询")
public GenericsAjaxResult<ModelDependency> queryById(@PathVariable("id") Integer id) {
return genericsSuccess(this.modelDependencyService.queryById(id));
}

/**
* 新增数据
*
* @param modelDependency 实体
* @return 新增结果
*/
@PostMapping
public GenericsAjaxResult<ModelDependency> add(@RequestBody ModelDependency modelDependency) {
return genericsSuccess(this.modelDependencyService.insert(modelDependency));
}

/**
* 编辑数据
*
* @param modelDependency 实体
* @return 编辑结果
*/
@PutMapping
public GenericsAjaxResult<ModelDependency> edit(@RequestBody ModelDependency modelDependency) {
return genericsSuccess(this.modelDependencyService.update(modelDependency));
}

/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping("{id}")
@ApiOperation("删除模型依赖")
public GenericsAjaxResult<String> deleteById(@PathVariable("id") Integer id) {
return genericsSuccess(this.modelDependencyService.removeById(id));
}


@PostMapping("/queryModelAtlas")
@ApiOperation("根据模型id与版本两个属性得到模型的演化图谱")
public GenericsAjaxResult<ModelDependcyTreeVo> queryModelAtlas(@RequestBody ModelDependency modelDependency) throws Exception {
return genericsSuccess(this.modelDependencyService.getModelDependencyTree(modelDependency));
}
}


+ 2
- 1
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelsVersionController.java View File

@@ -11,6 +11,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;


import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;


@@ -129,7 +130,7 @@ public class ModelsVersionController extends BaseController {
@DeleteMapping("/deleteVersion") @DeleteMapping("/deleteVersion")
@ApiOperation(value = "逻辑删除模型版本", notes = "根据模型ID和版本逻辑删除模型版本记录。") @ApiOperation(value = "逻辑删除模型版本", notes = "根据模型ID和版本逻辑删除模型版本记录。")
public GenericsAjaxResult<Map<Integer, String>> deleteModelsVersion(@RequestParam("models_id") Integer modelsId, public GenericsAjaxResult<Map<Integer, String>> deleteModelsVersion(@RequestParam("models_id") Integer modelsId,
@RequestParam("version") String version) {
@RequestParam("version") String version) throws IOException {
return genericsSuccess(this.modelsVersionService.deleteModelsVersion(modelsId, version)); return genericsSuccess(this.modelsVersionService.deleteModelsVersion(modelsId, version));
} }




+ 213
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java View File

@@ -0,0 +1,213 @@
package com.ruoyi.platform.domain;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import java.util.Date;
import java.io.Serializable;

/**
* (DevEnvironment)实体类
*
* @author Xidaray
* @since 2024-06-03 15:17:37
*/
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class DevEnvironment implements Serializable {
private static final long serialVersionUID = 936999018935545992L;
/**
* 主键
*/
private Integer id;
/**
* 编辑器名称
*/
private String name;
/**
* 状态
*/
private String status;
/**
* 计算资源
*/
private String computingResource;
/**
* 资源规格
*/
private String standard;
/**
* 环境变量
*/
private String envVariable;
/**
* 所用镜像
*/
private String image;
/**
* 对应数据集
*/
private String dataset;
/**
* 对应模型
*/
private String model;
/**
* 备用字段1
*/
private String altField1;
/**
* 备用字段2
*/
private String altField2;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 状态,0失效1生效
*/
private Integer state;


public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}

public String getComputingResource() {
return computingResource;
}

public void setComputingResource(String computingResource) {
this.computingResource = computingResource;
}

public String getStandard() {
return standard;
}

public void setStandard(String standard) {
this.standard = standard;
}

public String getEnvVariable() {
return envVariable;
}

public void setEnvVariable(String envVariable) {
this.envVariable = envVariable;
}

public String getImage() {
return image;
}

public void setImage(String image) {
this.image = image;
}

public String getDataset() {
return dataset;
}

public void setDataset(String dataset) {
this.dataset = dataset;
}

public String getModel() {
return model;
}

public void setModel(String model) {
this.model = model;
}

public String getAltField1() {
return altField1;
}

public void setAltField1(String altField1) {
this.altField1 = altField1;
}

public String getAltField2() {
return altField2;
}

public void setAltField2(String altField2) {
this.altField2 = altField2;
}

public String getCreateBy() {
return createBy;
}

public void setCreateBy(String createBy) {
this.createBy = createBy;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

public String getUpdateBy() {
return updateBy;
}

public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}

public Date getUpdateTime() {
return updateTime;
}

public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}

public Integer getState() {
return state;
}

public void setState(Integer state) {
this.state = state;
}

}


+ 247
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ModelDependency.java View File

@@ -0,0 +1,247 @@
package com.ruoyi.platform.domain;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.swagger.annotations.ApiModelProperty;

import java.util.Date;
import java.io.Serializable;

/**
* (ModelDependency)实体类
*
* @author Xidaray
* @since 2024-05-29 13:51:23
*/
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class ModelDependency implements Serializable {
private static final long serialVersionUID = -86753423714028539L;
/**
* 主键
*/
private Integer id;
/**
* 当前模型id
*/
private Integer currentModelId;
/**
* 实验实例id
*/
private Integer expInsId;

/**
* 父模型
*/
private String parentModels;
/**
* 引用项目
*/
private String refItem;
/**
* 训练任务
*/
private String trainTask;
/**
* 训练数据集
*/
private String trainDataset;
/**
* 训练参数
*/
private String trainParams;
/**
* 训练镜像
*/
private String trainImage;
/**
* 测试数据集
*/
private String testDataset;
/**
* 依赖项目
*/
private String projectDependency;

/**
* 版本
*/
private String version;


/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 状态,0失效1生效
*/
private Integer state;

private Long workflowId;
private Models models;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getCurrentModelId() {
return currentModelId;
}

public void setCurrentModelId(Integer currentModelId) {
this.currentModelId = currentModelId;
}

public Integer getExpInsId() {
return expInsId;
}

public void setExpInsId(Integer expInsId) {
this.expInsId = expInsId;
}

public String getParentModels() {
return parentModels;
}

public void setParentModels(String parentModels) {
this.parentModels = parentModels;
}

public String getRefItem() {
return refItem;
}

public void setRefItem(String refItem) {
this.refItem = refItem;
}

public String getTrainTask() {
return trainTask;
}

public void setTrainTask(String trainTask) {
this.trainTask = trainTask;
}

public String getTrainDataset() {
return trainDataset;
}

public void setTrainDataset(String trainDataset) {
this.trainDataset = trainDataset;
}

public String getTrainParams() {
return trainParams;
}

public void setTrainParams(String trainParams) {
this.trainParams = trainParams;
}

public String getTrainImage() {
return trainImage;
}

public void setTrainImage(String trainImage) {
this.trainImage = trainImage;
}

public String getTestDataset() {
return testDataset;
}

public void setTestDataset(String testDataset) {
this.testDataset = testDataset;
}

public String getProjectDependency() {
return projectDependency;
}

public void setProjectDependency(String projectDependency) {
this.projectDependency = projectDependency;
}

public String getVersion() {
return version;
}

public void setVersion(String version) {
this.version = version;
}

public String getCreateBy() {
return createBy;
}

public void setCreateBy(String createBy) {
this.createBy = createBy;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

public String getUpdateBy() {
return updateBy;
}

public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}

public Date getUpdateTime() {
return updateTime;
}

public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}

public Integer getState() {
return state;
}

public void setState(Integer state) {
this.state = state;
}

public Long getWorkflowId() {
return workflowId;
}

public void setWorkflowId(Long workflowId) {
this.workflowId = workflowId;
}

public Models getModels() {
return models;
}

public void setModels(Models models) {
this.models = models;
}
}


+ 14
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/dependencydomain/ProjectDepency.java View File

@@ -0,0 +1,14 @@
package com.ruoyi.platform.domain.dependencydomain;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;

import java.io.Serializable;
@Data
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class ProjectDepency implements Serializable {
private String url;
private String name;
private String branch;
}

+ 17
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/dependencydomain/TrainTaskDepency.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.platform.domain.dependencydomain;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;

import java.io.Serializable;
@Data
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class TrainTaskDepency implements Serializable {
//训练任务名
private String name;
//实例id
private Integer insId;
//节点Id
private String taskId;
}

+ 84
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java View File

@@ -0,0 +1,84 @@
package com.ruoyi.platform.mapper;

import com.ruoyi.platform.domain.DevEnvironment;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.domain.Pageable;
import java.util.List;

/**
* (DevEnvironment)表数据库访问层
*
* @author Xidaray
* @since 2024-06-03 15:17:37
*/
public interface DevEnvironmentDao {

/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
DevEnvironment queryById(Integer id);

/**
* 查询指定行数据
*
* @param devEnvironment 查询条件
* @param pageable 分页对象
* @return 对象列表
*/
List<DevEnvironment> queryAllByLimit(@Param("devEnvironment") DevEnvironment devEnvironment, @Param("pageable") Pageable pageable);

/**
* 统计总行数
*
* @param devEnvironment 查询条件
* @return 总行数
*/
long count(@Param("devEnvironment") DevEnvironment devEnvironment);

/**
* 新增数据
*
* @param devEnvironment 实例对象
* @return 影响行数
*/
int insert(@Param("devEnvironment") DevEnvironment devEnvironment);

/**
* 批量新增数据(MyBatis原生foreach方法)
*
* @param entities List<DevEnvironment> 实例对象列表
* @return 影响行数
*/
int insertBatch(@Param("entities") List<DevEnvironment> entities);

/**
* 批量新增或按主键更新数据(MyBatis原生foreach方法)
*
* @param entities List<DevEnvironment> 实例对象列表
*
* @return 影响行数
* @throws org.springframework.jdbc.BadSqlGrammarException 入参是空List的时候会抛SQL语句错误的异常,请自行校验入参
*/
int insertOrUpdateBatch(@Param("entities") List<DevEnvironment> entities);

/**
* 修改数据
*
* @param devEnvironment 实例对象
* @return 影响行数
*/
int update(@Param("devEnvironment") DevEnvironment devEnvironment);

/**
* 通过主键删除数据
*
* @param id 主键
* @return 影响行数
*/
int deleteById(Integer id);

}


+ 88
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ModelDependencyDao.java View File

@@ -0,0 +1,88 @@
package com.ruoyi.platform.mapper;

import com.ruoyi.platform.domain.ModelDependency;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.domain.Pageable;
import java.util.List;

/**
* (ModelDependency)表数据库访问层
*
* @author Xidaray
* @since 2024-05-29 13:51:23
*/
public interface ModelDependencyDao {

/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
ModelDependency queryById(Integer id);

/**
* 查询指定行数据
*
* @param modelDependency 查询条件
* @param pageable 分页对象
* @return 对象列表
*/
List<ModelDependency> queryAllByLimit(ModelDependency modelDependency, @Param("pageable") Pageable pageable);



/**
* 统计总行数
*
* @param modelDependency 查询条件
* @return 总行数
*/
long count(ModelDependency modelDependency);

/**
* 新增数据
*
* @param modelDependency 实例对象
* @return 影响行数
*/
int insert(ModelDependency modelDependency);

/**
* 批量新增数据(MyBatis原生foreach方法)
*
* @param entities List<ModelDependency> 实例对象列表
* @return 影响行数
*/
int insertBatch(@Param("entities") List<ModelDependency> entities);

/**
* 批量新增或按主键更新数据(MyBatis原生foreach方法)
*
* @param entities List<ModelDependency> 实例对象列表
* @return 影响行数
* @throws org.springframework.jdbc.BadSqlGrammarException 入参是空List的时候会抛SQL语句错误的异常,请自行校验入参
*/
int insertOrUpdateBatch(@Param("entities") List<ModelDependency> entities);

/**
* 修改数据
*
* @param modelDependency 实例对象
* @return 影响行数
*/
int update(ModelDependency modelDependency);

/**
* 通过主键删除数据
*
* @param id 主键
* @return 影响行数
*/
int deleteById(Integer id);

List<ModelDependency> queryByModelDependency(@Param("modelDependency") ModelDependency modelDependency);

List<ModelDependency> queryChildrenByVersionId(@Param("model_id")String modelId, @Param("version")String version);
}


+ 50
- 9
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java View File

@@ -1,24 +1,26 @@
package com.ruoyi.platform.scheduling; package com.ruoyi.platform.scheduling;


import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.domain.Experiment; import com.ruoyi.platform.domain.Experiment;
import com.ruoyi.platform.domain.ExperimentIns; import com.ruoyi.platform.domain.ExperimentIns;
import com.ruoyi.platform.domain.ModelDependency;
import com.ruoyi.platform.mapper.ExperimentDao; import com.ruoyi.platform.mapper.ExperimentDao;
import com.ruoyi.platform.mapper.ExperimentInsDao; import com.ruoyi.platform.mapper.ExperimentInsDao;
import com.ruoyi.platform.mapper.ModelDependencyDao;
import com.ruoyi.platform.service.ExperimentInsService; import com.ruoyi.platform.service.ExperimentInsService;
import com.ruoyi.platform.service.ExperimentService;
import com.ruoyi.platform.utils.JsonUtils;
import com.ruoyi.system.api.model.LoginUser;
import io.swagger.models.auth.In;
import com.ruoyi.platform.service.ModelDependencyService;
import com.ruoyi.platform.utils.JacksonUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;


import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;


@Component() @Component()
public class ExperimentInstanceStatusTask { public class ExperimentInstanceStatusTask {
@@ -28,7 +30,8 @@ public class ExperimentInstanceStatusTask {
private ExperimentDao experimentDao; private ExperimentDao experimentDao;
@Resource @Resource
private ExperimentInsDao experimentInsDao; private ExperimentInsDao experimentInsDao;

@Resource
private ModelDependencyDao modelDependencyDao;
private List<Integer> experimentIds = new ArrayList<>(); private List<Integer> experimentIds = new ArrayList<>();


@Scheduled(cron = "0/30 * * * * ?") // 每30S执行一次 @Scheduled(cron = "0/30 * * * * ?") // 每30S执行一次
@@ -55,12 +58,50 @@ public class ExperimentInstanceStatusTask {
updateList.add(experimentIns); updateList.add(experimentIns);


} }
experimentInsDao.update(experimentIns);
// experimentInsDao.update(experimentIns);
} }


} }
if (updateList.size() > 0){ if (updateList.size() > 0){
experimentInsDao.insertOrUpdateBatch(updateList); experimentInsDao.insertOrUpdateBatch(updateList);

//遍历模型关系表,找到
List<ModelDependency> modelDependencyList = new ArrayList<ModelDependency>();
for (ExperimentIns experimentIns : updateList){
ModelDependency modelDependencyquery = new ModelDependency();
modelDependencyquery.setExpInsId(experimentIns.getId());
modelDependencyquery.setState(2);

List<ModelDependency> modelDependencyListquery = modelDependencyDao.queryByModelDependency(modelDependencyquery);
if (modelDependencyListquery==null||modelDependencyListquery.size()==0){
continue;
}
ModelDependency modelDependency = modelDependencyListquery.get(0);
//查看状态,
if (StringUtils.equals("Failed",experimentIns.getStatus())){
//取出节点状态
String trainTask = modelDependency.getTrainTask();
Map<String, Object> trainMap = JacksonUtil.parseJSONStr2Map(trainTask);
String task_id = (String) trainMap.get("task_id");
if (StringUtils.isEmpty(task_id)){
continue;
}
String nodesStatus = experimentIns.getNodesStatus();
Map<String, Object> nodeMaps = JacksonUtil.parseJSONStr2Map(nodesStatus);
Map<String, Object> nodeMap = JacksonUtil.parseJSONStr2Map(JacksonUtil.toJSONString(nodeMaps.get(task_id)));

if (nodeMap==null){
continue;
}
if (!StringUtils.equals("Succeeded",(String)nodeMap.get("phase"))){
modelDependency.setState(0);
modelDependencyList.add(modelDependency);
}
}
}
if (modelDependencyList.size()>0) {
modelDependencyDao.insertOrUpdateBatch(modelDependencyList);
}
} }


} }


+ 57
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java View File

@@ -0,0 +1,57 @@
package com.ruoyi.platform.service;

import com.ruoyi.platform.domain.DevEnvironment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

/**
* (DevEnvironment)表服务接口
*
* @author Xidaray
* @since 2024-06-03 15:17:37
*/
public interface DevEnvironmentService {

/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
DevEnvironment queryById(Integer id);

/**
* 分页查询
*
* @param devEnvironment 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
Page<DevEnvironment> queryByPage(DevEnvironment devEnvironment, PageRequest pageRequest);

/**
* 新增数据
*
* @param devEnvironment 实例对象
* @return 实例对象
*/
DevEnvironment insert(DevEnvironment devEnvironment);

/**
* 修改数据
*
* @param devEnvironment 实例对象
* @return 实例对象
*/
DevEnvironment update(DevEnvironment devEnvironment);

/**
* 通过主键删除数据
*
* @param id 主键
* @return 是否成功
*/
boolean deleteById(Integer id);

String removeById(Integer id);
}

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

@@ -100,7 +100,7 @@ public interface ExperimentInsService {


/** /**
* 查询非终止态的实例 * 查询非终止态的实例
* @return
*
*/ */
List<ExperimentIns> queryByExperimentIsNotTerminated(); List<ExperimentIns> queryByExperimentIsNotTerminated();




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

@@ -48,7 +48,7 @@ public interface ExperimentService {
* @param experiment 实例对象 * @param experiment 实例对象
* @return 实例对象 * @return 实例对象
*/ */
Experiment update(Experiment experiment) throws IOException;
Experiment update(Experiment experiment) throws Exception;


/** /**
* 通过主键删除数据 * 通过主键删除数据


+ 6
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/JupyterService.java View File

@@ -1,5 +1,7 @@
package com.ruoyi.platform.service; package com.ruoyi.platform.service;


import com.ruoyi.platform.vo.FrameLogPathVo;

import java.io.InputStream; import java.io.InputStream;


public interface JupyterService { public interface JupyterService {
@@ -8,4 +10,8 @@ public interface JupyterService {
void upload(InputStream inputStream); void upload(InputStream inputStream);


void mlflow(); void mlflow();

String runJupyterService(Integer id);

String stopJupyterService(Integer id) throws Exception;
} }

+ 65
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ModelDependencyService.java View File

@@ -0,0 +1,65 @@
package com.ruoyi.platform.service;

import com.ruoyi.platform.domain.ModelDependency;
import com.ruoyi.platform.vo.ModelDependcyTreeVo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

import java.io.IOException;
import java.util.List;

/**
* (ModelDependency)表服务接口
*
* @author Xidaray
* @since 2024-05-29 13:51:23
*/
public interface ModelDependencyService {

/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
ModelDependency queryById(Integer id);

/**
* 分页查询
*
* @param modelDependency 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
Page<ModelDependency> queryByPage(ModelDependency modelDependency, PageRequest pageRequest);

/**
* 修改数据
*
* @param modelDependency 实例对象
* @return 实例对象
*/
ModelDependency update(ModelDependency modelDependency);

/**
* 新增数据
*
* @param modelDependency 实例对象
* @return 实例对象
*/
ModelDependency insert(ModelDependency modelDependency);

/**
* 通过主键删除数据
*
* @param id 主键
* @return 是否成功
*/
boolean deleteById(Integer id);

String removeById(Integer id);

List<ModelDependency> queryByModelDependency(ModelDependency modelDependency) throws IOException;

ModelDependcyTreeVo getModelDependencyTree(ModelDependency modelDependency) throws Exception;
}

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

@@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.ModelsVersion;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;


import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;


@@ -68,7 +69,7 @@ public interface ModelsVersionService {


Map<String,Object> queryByModelsIdAndVersion(Integer modelsId, String version); Map<String,Object> queryByModelsIdAndVersion(Integer modelsId, String version);


Map<Integer, String> deleteModelsVersion(Integer modelsId, String version);
Map<Integer, String> deleteModelsVersion(Integer modelsId, String version) throws IOException;


String addModelVersions(List<ModelsVersion> modelsVersions) throws Exception; String addModelVersions(List<ModelsVersion> modelsVersions) throws Exception;




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

@@ -353,22 +353,15 @@ public class DatasetServiceImpl implements DatasetService {
public void checkDeclaredName(Dataset insert) throws Exception { public void checkDeclaredName(Dataset insert) throws Exception {
Dataset existingDataset = datasetDao.findByName(insert.getName()); Dataset existingDataset = datasetDao.findByName(insert.getName());
if (existingDataset != null) { if (existingDataset != null) {
// Check if the found dataset is not the same as the one being inserted
// This is important if you are using this method for both insert and update operations
// You may need an identifier check here, e.g., if 'insert' has an ID and it's the same as 'existingDataset'
if (insert.getId() != null && insert.getId().equals(existingDataset.getId())) { if (insert.getId() != null && insert.getId().equals(existingDataset.getId())) {
// This is the same dataset, no duplicate name issue for update operation
// 相同数据集,无需判断
return; return;
} }


// Now we know there's another dataset with the same name
Field[] fields = Dataset.class.getDeclaredFields(); Field[] fields = Dataset.class.getDeclaredFields();

for (Field field : fields) { for (Field field : fields) {
field.setAccessible(true); // Make private fields accessible

field.setAccessible(true);
if ("name".equals(field.getName()) && field.isAnnotationPresent(CheckDuplicate.class)) { if ("name".equals(field.getName()) && field.isAnnotationPresent(CheckDuplicate.class)) {
// If the field is 'name' and is marked with CheckDuplicate annotation
CheckDuplicate annotation = field.getAnnotation(CheckDuplicate.class); CheckDuplicate annotation = field.getAnnotation(CheckDuplicate.class);
throw new Exception("重复的数据集名称: " + insert.getName() + ". " + annotation.message()); throw new Exception("重复的数据集名称: " + insert.getName() + ". " + annotation.message());
} }


+ 125
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java View File

@@ -0,0 +1,125 @@
package com.ruoyi.platform.service.impl;

import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.domain.DevEnvironment;
import com.ruoyi.platform.mapper.DevEnvironmentDao;
import com.ruoyi.platform.service.DevEnvironmentService;
import com.ruoyi.platform.service.JupyterService;
import com.ruoyi.platform.utils.JacksonUtil;
import com.ruoyi.system.api.model.LoginUser;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;

import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;

/**
* (DevEnvironment)表服务实现类
*
* @author Xidaray
* @since 2024-06-03 15:17:37
*/
@Service("devEnvironmentService")
public class DevEnvironmentServiceImpl implements DevEnvironmentService {
@Resource
private DevEnvironmentDao devEnvironmentDao;


@Resource
private JupyterService jupyterService;


/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Override
public DevEnvironment queryById(Integer id) {
return this.devEnvironmentDao.queryById(id);
}

/**
* 分页查询
*
* @param devEnvironment 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
@Override
public Page<DevEnvironment> queryByPage(DevEnvironment devEnvironment, PageRequest pageRequest) {
long total = this.devEnvironmentDao.count(devEnvironment);
return new PageImpl<>(this.devEnvironmentDao.queryAllByLimit(devEnvironment, pageRequest), pageRequest, total);
}

/**
* 新增数据
*
* @param devEnvironment 实例对象
* @return 实例对象
*/
@Override
public DevEnvironment insert(DevEnvironment devEnvironment) {
//插入预备,此时不需要判断版本重复
LoginUser loginUser = SecurityUtils.getLoginUser();
devEnvironment.setCreateBy(loginUser.getUsername());
devEnvironment.setUpdateBy(loginUser.getUsername());
devEnvironment.setUpdateTime(new Date());
devEnvironment.setCreateTime(new Date());
this.devEnvironmentDao.insert(devEnvironment);
return devEnvironment;
}

/**
* 修改数据
*
* @param devEnvironment 实例对象
* @return 实例对象
*/
@Override
public DevEnvironment update(DevEnvironment devEnvironment) {
LoginUser loginUser = SecurityUtils.getLoginUser();
devEnvironment.setUpdateBy(loginUser.getUsername());
devEnvironment.setUpdateTime(new Date());
this.devEnvironmentDao.update(devEnvironment);
return this.queryById(devEnvironment.getId());
}

/**
* 通过主键删除数据
*
* @param id 主键
* @return 是否成功
*/
@Override
public boolean deleteById(Integer id) {
return this.devEnvironmentDao.deleteById(id) > 0;
}

@Override
public String removeById(Integer id) {
DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id);
if (devEnvironment == null){
return "开发环境信息不存在";
}

//判断权限,只有admin和创建者本身可以删除该数据集
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser.getUsername();
String createdBy = devEnvironment.getCreateBy();
if (!(StringUtils.equals(username,"admin") || StringUtils.equals(username,createdBy))){
return "无权限删除该开发环境";
}

devEnvironment.setState(0);
return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败";
}


}

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

@@ -427,7 +427,6 @@ public class ExperimentInsServiceImpl implements ExperimentInsService {
throw new RuntimeException("日志为空。"); throw new RuntimeException("日志为空。");
} }
//返回日志内容 //返回日志内容

return experimentInsLog; return experimentInsLog;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("查询实验日志失败: " + e.getMessage(), e); throw new RuntimeException("查询实验日志失败: " + e.getMessage(), e);
@@ -554,7 +553,7 @@ public class ExperimentInsServiceImpl implements ExperimentInsService {
// 查询具有相同状态的实例数量 // 查询具有相同状态的实例数量
Long count = experimentInsDao.count(experimentIns); Long count = experimentInsDao.count(experimentIns);


// 将状态及其对应的实例数量放入映射
// 将状态及其对应的实例数量放入map
statusCountMap.put(status.toString(), count); statusCountMap.put(status.toString(), count);
} }


@@ -580,8 +579,9 @@ public class ExperimentInsServiceImpl implements ExperimentInsService {
flag = StringUtils.equals("Terminated", (String) workflowMap.get("phase")); flag = StringUtils.equals("Terminated", (String) workflowMap.get("phase"));
} }
} }

return flag; return flag;
} }


} }



+ 233
- 22
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java View File

@@ -2,14 +2,12 @@ package com.ruoyi.platform.service.impl;


import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.annotations.CheckDuplicate; import com.ruoyi.platform.annotations.CheckDuplicate;
import com.ruoyi.platform.domain.Experiment;
import com.ruoyi.platform.domain.ExperimentIns;
import com.ruoyi.platform.domain.Workflow;
import com.ruoyi.platform.domain.*;
import com.ruoyi.platform.domain.dependencydomain.ProjectDepency;
import com.ruoyi.platform.domain.dependencydomain.TrainTaskDepency;
import com.ruoyi.platform.mapper.ExperimentDao; import com.ruoyi.platform.mapper.ExperimentDao;
import com.ruoyi.platform.mapper.ExperimentInsDao; import com.ruoyi.platform.mapper.ExperimentInsDao;
import com.ruoyi.platform.service.ExperimentInsService;
import com.ruoyi.platform.service.ExperimentService;
import com.ruoyi.platform.service.WorkflowService;
import com.ruoyi.platform.service.*;
import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.HttpUtils;
import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.utils.JacksonUtil;
import com.ruoyi.platform.utils.JsonUtils; import com.ruoyi.platform.utils.JsonUtils;
@@ -42,8 +40,12 @@ public class ExperimentServiceImpl implements ExperimentService {


@Resource @Resource
private ExperimentInsDao experimentInsDao; private ExperimentInsDao experimentInsDao;


@Resource
private ModelsService modelsService;
@Resource
private DatasetService datasetService;
@Resource
private ModelDependencyService modelDependencyService;


@Resource @Resource
@Lazy @Lazy
@@ -151,8 +153,9 @@ public class ExperimentServiceImpl implements ExperimentService {
* @return 实例对象 * @return 实例对象
*/ */
@Override @Override
public Experiment update(Experiment experiment) throws IOException {
public Experiment update(Experiment experiment) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser(); LoginUser loginUser = SecurityUtils.getLoginUser();
checkDeclaredName(experiment);
experiment.setUpdateBy(loginUser.getUsername()); experiment.setUpdateBy(loginUser.getUsername());
experiment.setUpdateTime(new Date()); experiment.setUpdateTime(new Date());
this.experimentDao.update(experiment); this.experimentDao.update(experiment);
@@ -207,12 +210,9 @@ public class ExperimentServiceImpl implements ExperimentService {
public Experiment runExperiment(Integer id) throws Exception { public Experiment runExperiment(Integer id) throws Exception {
//先查出实验记录 //先查出实验记录
Experiment experiment = this.queryById(id); Experiment experiment = this.queryById(id);

if (experiment == null) { if (experiment == null) {
System.out.println("No experiment"); System.out.println("No experiment");
} }


Workflow workflow = workflowService.queryById(experiment.getWorkflowId()); Workflow workflow = workflowService.queryById(experiment.getWorkflowId());
if(workflow == null) { if(workflow == null) {
throw new RuntimeException("流水线不存在,请先创建流水线"); throw new RuntimeException("流水线不存在,请先创建流水线");
@@ -226,13 +226,14 @@ public class ExperimentServiceImpl implements ExperimentService {
throw new RuntimeException("转换流水线失败"); throw new RuntimeException("转换流水线失败");
} }
Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes); Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes);

// 组装运行接口json // 组装运行接口json
Map<String, Object> runReqMap = new HashMap<>(); Map<String, Object> runReqMap = new HashMap<>();
runReqMap.put("data", converMap.get("data")); runReqMap.put("data", converMap.get("data"));
//这里全局参数是一个json数组,需要转换成一个list<Map> //这里全局参数是一个json数组,需要转换成一个list<Map>
List<Map<String, Object>> params = JacksonUtil.parseJSONStr2MapList(StringUtils.isEmpty(experiment.getGlobalParam()) ? "[]" : experiment.getGlobalParam()); List<Map<String, Object>> params = JacksonUtil.parseJSONStr2MapList(StringUtils.isEmpty(experiment.getGlobalParam()) ? "[]" : experiment.getGlobalParam());
runReqMap.put("params", params); runReqMap.put("params", params);
//// 实验字段的Map,不要写成一行!否则会返回null
// 实验字段的Map,不要写成一行!否则会返回null
Map<String, Object> experimentMap = new HashMap<>(); Map<String, Object> experimentMap = new HashMap<>();
experimentMap.put("name", "experiment-"+experiment.getId()); experimentMap.put("name", "experiment-"+experiment.getId());
runReqMap.put("experiment", experimentMap); runReqMap.put("experiment", experimentMap);
@@ -246,14 +247,11 @@ public class ExperimentServiceImpl implements ExperimentService {
} }
Map<String, Object> runResMap = JsonUtils.jsonToMap(runRes); Map<String, Object> runResMap = JsonUtils.jsonToMap(runRes);
Map<String, Object> data = (Map<String, Object>) runResMap.get("data"); Map<String, Object> data = (Map<String, Object>) runResMap.get("data");

//判断data为空 //判断data为空
if (data == null || MapUtils.isEmpty(data)) { if (data == null || MapUtils.isEmpty(data)) {
throw new RuntimeException("Failed to run workflow."); throw new RuntimeException("Failed to run workflow.");
} }

Map<String, Object> metadata = (Map<String, Object>) data.get("metadata"); Map<String, Object> metadata = (Map<String, Object>) data.get("metadata");

// 插入记录到实验实例表 // 插入记录到实验实例表
ExperimentIns experimentIns = new ExperimentIns(); ExperimentIns experimentIns = new ExperimentIns();
experimentIns.setExperimentId(experiment.getId()); experimentIns.setExperimentId(experiment.getId());
@@ -262,24 +260,237 @@ public class ExperimentServiceImpl implements ExperimentService {
experimentIns.setStatus("Pending"); experimentIns.setStatus("Pending");


//传入实验全局参数 //传入实验全局参数

experimentIns.setGlobalParam(experiment.getGlobalParam()); experimentIns.setGlobalParam(experiment.getGlobalParam());

//替换argoInsName //替换argoInsName
String outputString = JsonUtils.mapToJson(output); String outputString = JsonUtils.mapToJson(output);
experimentIns.setNodesResult(outputString.replace("{{workflow.name}}", (String) metadata.get("name"))); experimentIns.setNodesResult(outputString.replace("{{workflow.name}}", (String) metadata.get("name")));
//插入ExperimentIns表中 //插入ExperimentIns表中
experimentInsService.insert(experimentIns);
ExperimentIns insert = experimentInsService.insert(experimentIns);

//插入到模型依赖关系表
//得到dependendcy
Map<String, Object> converMap2 = JsonUtils.jsonToMap(JacksonUtil.replaceInAarry(convertRes, params));
Map<String ,Object> dependendcy = (Map<String, Object>)converMap2.get("model_dependency");
Map<String ,Object> trainInfo = (Map<String, Object>)converMap2.get("component_info");
insertModelDependency(dependendcy,trainInfo,insert.getId(),experiment.getName());


}catch (Exception e){ }catch (Exception e){
throw new RuntimeException(e); throw new RuntimeException(e);
} }
List<ExperimentIns> updatedExperimentInsList = experimentInsService.getByExperimentId(id); List<ExperimentIns> updatedExperimentInsList = experimentInsService.getByExperimentId(id);

experiment.setExperimentInsList(updatedExperimentInsList); experiment.setExperimentInsList(updatedExperimentInsList);

return experiment; return experiment;
} }
private void insertModelDependency(Map<String ,Object> dependendcy,Map<String ,Object> trainInfo, Integer experimentInsId, String experimentName) throws Exception {
Iterator<Map.Entry<String, Object>> dependendcyIterator = dependendcy.entrySet().iterator();
Map<String, Object> modelTrain = (Map<String, Object>) trainInfo.get("model_train");
Map<String, Object> modelEvaluate = (Map<String, Object>) trainInfo.get("model_evaluate");
Map<String, Object> modelExport = (Map<String, Object>) trainInfo.get("model_export");
while (dependendcyIterator.hasNext()) {
ModelDependency modelDependency = new ModelDependency();
Map.Entry<String, Object> entry = dependendcyIterator.next();
Map<String, Object> modelDel = (Map<String, Object>) entry.getValue();
Map<String, Object> source = (Map<String, Object>) modelDel.get("source");
List<Map<String, Object>> test = (List<Map<String, Object>>) modelDel.get("test");
List<Map<String, Object>> target = (List<Map<String, Object>>) modelDel.get("target");
String sourceTaskId = (String) source.get("task_id");

Map<String, Object> modelTrainMap = (Map<String, Object>)modelTrain.get(sourceTaskId);
//处理project数据
Map<String, String> projectMap = (Map<String, String>) modelTrainMap.get("project");
ProjectDepency projectDepency = new ProjectDepency();
projectDepency.setBranch(projectMap.get("branch"));
String projectUrl = projectMap.get("url");
projectDepency.setUrl(projectUrl);
projectDepency.setName(projectUrl.substring(projectUrl.lastIndexOf('/') + 1, projectUrl.length() - 4));
//依赖项目
modelDependency.setProjectDependency(JsonUtils.objectToJson(projectDepency));
//处理镜像
Map<String, String> imagesMap = (Map<String, String>) modelTrainMap.get("image");
modelDependency.setTrainImage(imagesMap.get("name"));
List<Map<String, Object>> trainParamList = (List<Map<String, Object>>) modelTrainMap.get("params");
modelDependency.setTrainParams(JsonUtils.objectToJson(trainParamList));
//处理source数据
List<Map<String, Object>> modelsList = (List<Map<String, Object>>) modelTrainMap.get("models");
if (modelsList != null) {
for (int i = 0; i < modelsList.size(); i++) {
Map<String, Object> model = modelsList.get(i);
Models models = modelsService.queryById((Integer) model.get("model_id"));
if (models == null) {
throw new Exception("源模型不存在");
}
model.put("model_name", models.getName());
}
//父模型
modelDependency.setParentModels(JsonUtils.objectToJson(modelsList));
}
List<Map<String, Object>> datasetsList = (List<Map<String, Object>>) modelTrainMap.get("datasets");
if (datasetsList != null) {
for (int i = 0; i < datasetsList.size(); i++) {
Map<String, Object> datasets = datasetsList.get(i);
Dataset dataset = datasetService.queryById((Integer) datasets.get("dataset_id"));
if (dataset == null) {
throw new Exception("源数据集不存在");
}
datasets.put("dataset_name", dataset.getName());
}
}
//训练数据集
modelDependency.setTrainDataset(JsonUtils.objectToJson(datasetsList));
TrainTaskDepency trainTaskDepency = new TrainTaskDepency();
trainTaskDepency.setTaskId(sourceTaskId);
trainTaskDepency.setInsId(experimentInsId);
trainTaskDepency.setName(experimentName);
//训练任务
modelDependency.setTrainTask(JsonUtils.objectToJson(trainTaskDepency));
modelDependency.setExpInsId(experimentInsId);
List<Map<String, Object>> resultTestDatasets = new ArrayList<Map<String, Object>>();
//处理test数据
if (test != null) {
for(int i=0;i<test.size();i++){
Map<String, Object> testMap = test.get(i);
String testTaskId = (String) testMap.get("task_id");
Map<String, Object> evaluateMap = (Map<String, Object>) modelEvaluate.get(testTaskId);
List<Map<String, Object>> realDataSetList = (List<Map<String, Object>>) evaluateMap.get("datasets");
for(int j=0;j<realDataSetList.size();j++){
Map<String, Object> realDataSet = realDataSetList.get(j);
Dataset dataset = datasetService.queryById((Integer) realDataSet.get("dataset_id"));
if (dataset == null){
throw new Exception("源数据集不存在");
}
realDataSet.put("dataset_name", dataset.getName());
resultTestDatasets.add(realDataSet);
}
}

//测试数据集
modelDependency.setTestDataset(JsonUtils.objectToJson(resultTestDatasets));
}
//处理target数据
if (target != null) {
for (int i = 0; i < target.size(); i++) {
Map<String, Object> targetMap = target.get(i);
String targetaskId = (String) targetMap.get("task_id");
Map<String, Object> exportMap = (Map<String, Object>) modelExport.get(targetaskId);
List<Map<String, Object>> modelTargetList = (List<Map<String, Object>>) exportMap.get("models");
modelDependency.setState(2);
for (int j = 0; j < modelTargetList.size(); j++) {
Map<String, Object> model = modelTargetList.get(i);
modelDependency.setVersion((String) model.get("model_version"));
modelDependency.setCurrentModelId((Integer) model.get("model_id"));
//因为可能有多成果模型,多次插入
modelDependencyService.insert(modelDependency);
}
}
}else {
modelDependency.setState(2);
modelDependencyService.insert(modelDependency);
}

}
}

/**
* 被废弃的旧JSON
* @param experiment
* @return
* @throws Exception
*/
// private void insertModelDependency(Map<String ,Object> dependendcy,Map<String ,Object> trainInfo, Integer experimentInsId, String experimentName, List<Map<String, Object>> params) throws Exception {
// Iterator<Map.Entry<String, Object>> dependendcyIterator = dependendcy.entrySet().iterator();
// while (dependendcyIterator.hasNext()) {
// ModelDependency modelDependency = new ModelDependency();
// Map.Entry<String, Object> entry = dependendcyIterator.next();
// String key = entry.getKey();
// Map<String, Object> modelDel = (Map<String, Object>) entry.getValue();
// //处理project数据
// Map<String, String> projectMap = (Map<String, String>) modelDel.get("project");
// ProjectDepency projectDepency = new ProjectDepency();
// projectDepency.setBranch(projectMap.get("branch"));
// String projectUrl = projectMap.get("url");
// projectDepency.setUrl(projectUrl);
// projectDepency.setName(projectUrl.substring(projectUrl.lastIndexOf('/') + 1, projectUrl.length() - 4));
// //依赖项目
// modelDependency.setProjectDependency(JsonUtils.objectToJson(projectDepency));
// //处理source数据
// Map<String, Object> sourceMap = (Map<String, Object>) modelDel.get("source");
// List<Map<String, Object>> modelsList = (List<Map<String, Object>>) sourceMap.get("models");
// for(int i=0;i<modelsList.size();i++){
// Map<String, Object> model = modelsList.get(i);
// Models models = modelsService.queryById((Integer) model.get("model_id"));
// if (models == null){
// throw new Exception("源模型不存在");
// }
// model.put("model_name", models.getName());
// }
// //父模型
// modelDependency.setParentModels(JsonUtils.objectToJson(modelsList));
//
// List<Map<String, Object>> datasetsList = (List<Map<String, Object>>) sourceMap.get("datasets");
// for(int i=0;i<datasetsList.size();i++){
// Map<String, Object> datasets = datasetsList.get(i);
// Dataset dataset = datasetService.queryById((Integer) datasets.get("dataset_id"));
// if (dataset == null){
// throw new Exception("源数据集不存在");
// }
// datasets.put("dataset_name", dataset.getName());
// }
// //训练数据集
// modelDependency.setTrainDataset(JsonUtils.objectToJson(datasetsList));
//
// TrainTaskDepency trainTaskDepency = new TrainTaskDepency();
// trainTaskDepency.setTaskId(key);
// trainTaskDepency.setInsId(experimentInsId);
// trainTaskDepency.setName(experimentName);
// //训练任务
// modelDependency.setTrainTask(JsonUtils.objectToJson(trainTaskDepency));
// modelDependency.setExpInsId(experimentInsId);
// //处理test数据
// List<Map<String, Object>> testDatasetsList = (List<Map<String, Object>>) modelDel.get("test");
// List<Map<String, Object>> resultTestDatasets = new ArrayList<Map<String, Object>>();
// for(int i=0;i<testDatasetsList.size();i++){
// Map<String, Object> datasets = testDatasetsList.get(i);
// List<Map<String, Object>> realDataSetList = (List<Map<String, Object>>) datasets.get("datasets");
// for(int j=0;j<realDataSetList.size();j++){
// Map<String, Object> realDataSet = realDataSetList.get(j);
// Dataset dataset = datasetService.queryById((Integer) realDataSet.get("dataset_id"));
// if (dataset == null){
// throw new Exception("源数据集不存在");
// }
// realDataSet.put("dataset_name", dataset.getName());
// resultTestDatasets.add(realDataSet);
// }
//
// }
// //测试数据集
// modelDependency.setTestDataset(JsonUtils.objectToJson(resultTestDatasets));
//
// //检查是否存在target,如果存在说明在流水线用了节点导入,如果没有说明没有导入,等待手动push
// List<Map<String, Object>> modelTargetList = (List<Map<String, Object>>) modelDel.get("target");
// if (modelTargetList==null||modelTargetList.size()==0){
// modelDependency.setState(1);
// modelDependencyService.insert(modelDependency);
// }else {
// modelDependency.setState(2);
// for(int i=0;i<modelTargetList.size();i++){
// Map<String, Object> model = modelTargetList.get(i);
// String version = null;
// //可能是参数,必须从实验参数读取
// if (params != null) {
// for (Map<String, Object> param : params) {
// if (param.containsKey("param_name") && StringUtils.equals("model_version",(String) param.get("param_name"))) {
// version = param.get("param_value").toString();
// }
// }
// }
// modelDependency.setVersion(StringUtils.isEmpty(version)?(String)model.get("model_version"):version);
// modelDependency.setCurrentModelId((Integer) model.get("model_id"));
// //因为可能有多成果模型,多次插入
// modelDependencyService.insert(modelDependency);
// }
// }
// }
// }


@Override @Override
public Experiment addAndRunExperiment(Experiment experiment) throws Exception { public Experiment addAndRunExperiment(Experiment experiment) throws Exception {
@@ -332,7 +543,7 @@ public class ExperimentServiceImpl implements ExperimentService {
// 现在我们知道还有另一个具有相同名称的流水线 // 现在我们知道还有另一个具有相同名称的流水线
Field[] fields = Experiment.class.getDeclaredFields(); Field[] fields = Experiment.class.getDeclaredFields();
for (Field field : fields) { for (Field field : fields) {
field.setAccessible(true); // 使私有字段可访问
field.setAccessible(true);
if ("name".equals(field.getName()) && field.isAnnotationPresent(CheckDuplicate.class)) { if ("name".equals(field.getName()) && field.isAnnotationPresent(CheckDuplicate.class)) {
// 如果字段是“name”并且标记了CheckDuplicate注解 // 如果字段是“name”并且标记了CheckDuplicate注解
CheckDuplicate annotation = field.getAnnotation(CheckDuplicate.class); CheckDuplicate annotation = field.getAnnotation(CheckDuplicate.class);


+ 65
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java View File

@@ -1,7 +1,12 @@
package com.ruoyi.platform.service.impl; package com.ruoyi.platform.service.impl;


import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.domain.DevEnvironment;
import com.ruoyi.platform.mapper.DevEnvironmentDao;
import com.ruoyi.platform.service.DevEnvironmentService;
import com.ruoyi.platform.service.JupyterService; import com.ruoyi.platform.service.JupyterService;
import com.ruoyi.platform.utils.JacksonUtil;
import com.ruoyi.platform.utils.K8sClientUtil; import com.ruoyi.platform.utils.K8sClientUtil;
import com.ruoyi.platform.utils.MinioUtil; import com.ruoyi.platform.utils.MinioUtil;
import com.ruoyi.platform.utils.MlflowUtil; import com.ruoyi.platform.utils.MlflowUtil;
@@ -13,6 +18,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Map;


@Service @Service
public class JupyterServiceImpl implements JupyterService { public class JupyterServiceImpl implements JupyterService {
@@ -39,6 +45,15 @@ public class JupyterServiceImpl implements JupyterService {
@Resource @Resource
private MlflowUtil mlflowUtil; private MlflowUtil mlflowUtil;


@Resource
private DevEnvironmentDao devEnvironmentDao;

@Resource
private DevEnvironmentService devEnvironmentService;

@Resource
private RedisService redisService;

public JupyterServiceImpl(MinioUtil minioUtil) { public JupyterServiceImpl(MinioUtil minioUtil) {
this.minioUtil = minioUtil; this.minioUtil = minioUtil;
} }
@@ -53,6 +68,54 @@ public class JupyterServiceImpl implements JupyterService {
return masterIp + ":" + podPort; return masterIp + ":" + podPort;
} }


@Override
public String runJupyterService(Integer id) {
DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id);
String envName = devEnvironment.getName();
//TODO 设置环境变量

// 提取数据集,模型信息,得到数据集模型的path
Map<String, Object> dataset = JacksonUtil.parseJSONStr2Map(devEnvironment.getDataset());
String datasetPath = (String) dataset.get("path");
Map<String, Object> model = JacksonUtil.parseJSONStr2Map(devEnvironment.getModel());
String modelPath = (String) model.get("path");

LoginUser loginUser = SecurityUtils.getLoginUser();
String podName = loginUser.getUsername().toLowerCase() + "-editor-pod";
String pvcName = loginUser.getUsername().toLowerCase() + "-editor-pvc";
V1PersistentVolumeClaim pvc = k8sClientUtil.createPvc(namespace, pvcName, storage, storageClassName);

//TODO 设置镜像可配置,这里先用默认镜像启动pod

// 调用修改后的 createPod 方法,传入额外的参数
Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, datasetPath, modelPath);
return masterIp + ":" + podPort;


}

@Override
public String stopJupyterService(Integer id) throws Exception {
DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id);
if (devEnvironment==null){
throw new Exception("开发环境配置不存在");
}

LoginUser loginUser = SecurityUtils.getLoginUser();
String podName = loginUser.getUsername().toLowerCase() + "-editor-pod";

// 使用 Kubernetes API 删除 Pod
String deleteResult = k8sClientUtil.deletePod(podName, namespace);

// 检查 Pod 是否存在
boolean exists = k8sClientUtil.checkPodExists(podName, namespace);
if (exists) {
throw new Exception("Pod " + podName + " 删除失败");
}
return deleteResult + ",编辑器已停止";
}


@Override @Override
public void upload(InputStream inputStream) { public void upload(InputStream inputStream) {
try { try {
@@ -71,4 +134,6 @@ public class JupyterServiceImpl implements JupyterService {
} }






} }

+ 252
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelDependencyServiceImpl.java View File

@@ -0,0 +1,252 @@
package com.ruoyi.platform.service.impl;

import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.domain.*;
import com.ruoyi.platform.mapper.ModelDependencyDao;
import com.ruoyi.platform.service.*;
import com.ruoyi.platform.utils.JacksonUtil;
import com.ruoyi.platform.vo.ModelDependcyTreeVo;
import com.ruoyi.platform.vo.ModelVersionDependcyVo;
import com.ruoyi.system.api.model.LoginUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* (ModelDependency)表服务实现类
*
* @author Xidaray
* @since 2024-05-29 13:51:23
*/
@Service("modelDependencyService")
public class ModelDependencyServiceImpl implements ModelDependencyService {
@Resource
private ModelDependencyDao modelDependencyDao;

@Resource
private ModelsService modelsService;
@Resource
private ModelsVersionService modelsVersionService;
@Lazy
@Resource
private ExperimentService experimentService;
@Lazy
@Resource
private ExperimentInsService experimentInsService;
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Override
public ModelDependency queryById(Integer id) {
return this.modelDependencyDao.queryById(id);
}

/**
* 分页查询
*
* @param modelDependency 筛选条件
* @param pageRequest 分页对象
* @return 查询结果
*/
@Override
public Page<ModelDependency> queryByPage(ModelDependency modelDependency, PageRequest pageRequest) {
long total = this.modelDependencyDao.count(modelDependency);
return new PageImpl<>(this.modelDependencyDao.queryAllByLimit(modelDependency, pageRequest), pageRequest, total);
}

/**
* 根据对象查询
*
* @param modelDependency 筛选条件
* @return 查询结果
*/
@Override
public List<ModelDependency> queryByModelDependency(ModelDependency modelDependency) throws IOException {
List<ModelDependency> modelDependencyList = this.modelDependencyDao.queryByModelDependency(modelDependency);
return modelDependencyList;
}

@Override
public ModelDependcyTreeVo getModelDependencyTree(ModelDependency modelDependencyQuery) throws Exception {
//查询当前模型
modelDependencyQuery.setState(1);
List<ModelDependency> modelDependencyList = this.queryByModelDependency(modelDependencyQuery);
if (modelDependencyList==null || modelDependencyList.size()==0){
throw new Exception("当前模型依赖关系不存在");
}
ModelDependency modelDependency = modelDependencyList.get(0);
ModelDependcyTreeVo modelDependcyTreeVo = ModelDependencyConvertToTree(modelDependency);
//递归父模型
processParentModel(modelDependcyTreeVo);
//递归子模型
processChildrenModel(modelDependcyTreeVo);
return modelDependcyTreeVo;
}

/**
* 递归父模型
* @param modelDependcyTreeVo
*/
private void processParentModel(ModelDependcyTreeVo modelDependcyTreeVo) throws IOException {
if (modelDependcyTreeVo.getParentModelsMap() != null) {
List<Map<String, Object>> parentMaps = modelDependcyTreeVo.getParentModelsMap();
List<ModelDependcyTreeVo> ps = new ArrayList<ModelDependcyTreeVo>();
for (Map<String, Object> parent:parentMaps) {
Integer model_id = (Integer) parent.get("model_id");
String version = (String) parent.get("model_version");
ModelDependency modelDependencyQuery = new ModelDependency();
modelDependencyQuery.setVersion(version);
modelDependencyQuery.setCurrentModelId(model_id);
List<ModelDependency> modelDependencyList = this.queryByModelDependency(modelDependencyQuery);
if (modelDependencyList!=null&&modelDependencyList.size()>=0){
for (ModelDependency modelDependency:modelDependencyList){
ModelDependcyTreeVo modelDependencyTreeVoIn = ModelDependencyConvertToTree(modelDependency);
processParentModel(modelDependencyTreeVoIn);
ps.add(modelDependencyTreeVoIn);
}
}

}
modelDependcyTreeVo.setParentModels(ps);
}
}


private void processChildrenModel(ModelDependcyTreeVo modelDependcyTreeVo) throws IOException {
String version = modelDependcyTreeVo.getVersion();
Integer modelId = modelDependcyTreeVo.getCurrentModelId();
List<ModelDependcyTreeVo> cs = new ArrayList<ModelDependcyTreeVo>();
//查儿子们

List<ModelDependency> modelDependencyList = modelDependencyDao.queryChildrenByVersionId("\"model_id\":"+modelId, "\"model_version\":\""+version+"\"");
if (modelDependencyList!=null&&modelDependencyList.size()>=0){
for (ModelDependency modelDependency:modelDependencyList){
ModelDependcyTreeVo modelDependencyTreeVoIn = ModelDependencyConvertToTree(modelDependency);
processChildrenModel(modelDependencyTreeVoIn);
cs.add(modelDependencyTreeVoIn);
}
}
modelDependcyTreeVo.setChildrenModels(cs);
}

private ModelDependcyTreeVo ModelDependencyConvertToTree(ModelDependency modelDependency) throws IOException {
ModelDependcyTreeVo modelDependcyTreeVo = new ModelDependcyTreeVo();
modelDependcyTreeVo.setCurrentModelId(modelDependency.getCurrentModelId());
modelDependcyTreeVo.setExpInsId(modelDependency.getExpInsId());
modelDependcyTreeVo.setVersion(modelDependency.getVersion());
modelDependcyTreeVo.setRefItem(modelDependency.getRefItem());
modelDependcyTreeVo.setTrainTask(JacksonUtil.parseJSONStr2Map(modelDependency.getTrainTask()));
modelDependcyTreeVo.setTrainDataset(JacksonUtil.parseJSONStr2MapList(modelDependency.getTrainDataset()));
modelDependcyTreeVo.setTrainImage(modelDependency.getTrainImage());
modelDependcyTreeVo.setTrainParams(JacksonUtil.parseJSONStr2TList(modelDependency.getTrainParams(),Object.class,null));
modelDependcyTreeVo.setTestDataset(JacksonUtil.parseJSONStr2MapList(modelDependency.getTestDataset()));
modelDependcyTreeVo.setProjectDependency(JacksonUtil.parseJSONStr2Map(modelDependency.getProjectDependency()));
modelDependcyTreeVo.setParentModelsMap(JacksonUtil.parseJSONStr2MapList(modelDependency.getParentModels()));

/**
* 补充workFlow_id + 是否共有
*/
Integer currentModelId = modelDependency.getCurrentModelId();
Integer expInsId = modelDependency.getExpInsId();
Models models = modelsService.queryById(currentModelId);
ModelsVersion modelsVersionquery = new ModelsVersion();
modelsVersionquery.setModelsId(currentModelId);
modelsVersionquery.setVersion(modelDependency.getVersion());
ModelsVersion modelsVersion = modelsVersionService.queryByModelsVersion(modelsVersionquery);
ExperimentIns experimentIns = experimentInsService.queryById(expInsId);
Experiment experiment = experimentService.queryById(experimentIns.getExperimentId());
ModelVersionDependcyVo modelVersionDependcyVo = new ModelVersionDependcyVo();
modelVersionDependcyVo.setName(models.getName());
modelVersionDependcyVo.setAvailableRange(models.getAvailableRange());
modelVersionDependcyVo.setDescription(models.getDescription());
modelVersionDependcyVo.setModelTag(models.getModelTag());
modelVersionDependcyVo.setModelType(models.getModelType());
modelVersionDependcyVo.setModelTagName(models.getModelTagName());
modelVersionDependcyVo.setModelTypeName(models.getModelTypeName());
modelVersionDependcyVo.setFileName(modelsVersion.getFileName());
modelVersionDependcyVo.setFileSize(modelsVersion.getFileSize());
modelVersionDependcyVo.setUrl(modelsVersion.getUrl());
modelDependcyTreeVo.setWorkflowId(experiment.getWorkflowId());
modelDependcyTreeVo.setModelVersionDependcyVo(modelVersionDependcyVo);
return modelDependcyTreeVo;
}

/**
* 新增数据
*
* @param modelDependency 实例对象
* @return 实例对象
*/
@Override
public ModelDependency insert(ModelDependency modelDependency) {
//插入预备,此时不需要判断版本重复
LoginUser loginUser = SecurityUtils.getLoginUser();
modelDependency.setCreateBy(loginUser.getUsername());
modelDependency.setUpdateBy(loginUser.getUsername());
modelDependency.setUpdateTime(new Date());
modelDependency.setCreateTime(new Date());
this.modelDependencyDao.insert(modelDependency);
return modelDependency;
}

/**
* 修改数据
*
* @param modelDependency 实例对象
* @return 实例对象
*/
@Override
public ModelDependency update(ModelDependency modelDependency) {
LoginUser loginUser = SecurityUtils.getLoginUser();
modelDependency.setUpdateBy(loginUser.getUsername());
modelDependency.setUpdateTime(new Date());
this.modelDependencyDao.update(modelDependency);
return this.queryById(modelDependency.getId());
}

/**
* 通过主键删除数据
*
* @param id 主键
* @return 是否成功
*/
@Override
public boolean deleteById(Integer id) {
return this.modelDependencyDao.deleteById(id) > 0;
}

@Override
public String removeById(Integer id) {
ModelDependency modelDependency = this.modelDependencyDao.queryById(id);
if (modelDependency == null){
return "模型依赖信息不存在";
}

//判断权限,只有admin和创建者本身可以删除该数据集
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = loginUser.getUsername();
String createdBy = modelDependency.getCreateBy();
if (!(StringUtils.equals(username,"admin") || StringUtils.equals(username,createdBy))){
return "无权限删除";
}

modelDependency.setState(0);
return this.modelDependencyDao.update(modelDependency)>0?"删除成功":"删除失败";
}

}

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

@@ -192,6 +192,7 @@ public class ModelsServiceImpl implements ModelsService {
* *
* @param id models_version表的主键 * @param id models_version表的主键
* @return 文件内容 * @return 文件内容
*
*/ */


@Override @Override


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save