Browse Source

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

pull/75/head
西大锐 1 year ago
parent
commit
55eeb0a126
31 changed files with 1286 additions and 578 deletions
  1. +338
    -0
      react-ui/mock/model.ts
  2. +1
    -1
      react-ui/package.json
  3. +19
    -0
      react-ui/src/components/LabelValue/index.less
  4. +20
    -0
      react-ui/src/components/LabelValue/index.tsx
  5. +1
    -1
      react-ui/src/components/ModalTitle/index.less
  6. +3
    -3
      react-ui/src/components/ModalTitle/index.tsx
  7. +4
    -4
      react-ui/src/components/ParameterSelect/config.tsx
  8. +1
    -1
      react-ui/src/hooks/sessionStorage.ts
  9. +1
    -1
      react-ui/src/iconfont/iconfont.js
  10. +16
    -13
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  11. +22
    -22
      react-ui/src/pages/Dataset/config.tsx
  12. +8
    -14
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  13. +0
    -0
      react-ui/src/pages/Model/components/GraphLegend/index.less
  14. +28
    -10
      react-ui/src/pages/Model/components/GraphLegend/index.tsx
  15. +25
    -277
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  16. +328
    -0
      react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
  17. +3
    -4
      react-ui/src/pages/Model/components/NodeTooltips/index.less
  18. +76
    -12
      react-ui/src/pages/Model/components/NodeTooltips/index.tsx
  19. +20
    -27
      react-ui/src/pages/ModelDeployment/Info/index.less
  20. +27
    -169
      react-ui/src/pages/ModelDeployment/Info/index.tsx
  21. +91
    -0
      react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
  22. +16
    -0
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.less
  23. +147
    -0
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx
  24. +8
    -0
      react-ui/src/pages/ModelDeployment/components/UserGuide/index.less
  25. +32
    -0
      react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx
  26. +10
    -10
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
  27. +1
    -3
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  28. +8
    -0
      react-ui/src/services/modelDeployment/index.ts
  29. +22
    -1
      react-ui/src/utils/ui.tsx
  30. +9
    -4
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelDependencyServiceImpl.java
  31. +1
    -1
      ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ModelDependencyDaoMapper.xml

+ 338
- 0
react-ui/mock/model.ts View File

@@ -0,0 +1,338 @@
import { defineMock } from 'umi';

export default defineMock({
'POST /api/mmp/modelDependency/queryModelAtlas': {
code: 200,
msg: '操作成功',
data: {
current_model_id: 29,
exp_ins_id: 229,
version: 'v0.2.0',
ref_item: null,
train_task: {
name: '模型训练测试导出0529',
ins_id: 229,
task_id: 'model-train-5d76f002',
},
train_dataset: [
{
dataset_id: 20,
dataset_version: 'v0.1.0',
dataset_name: '手写体识别模型依赖测试训练数据集',
},
],
train_params: ['256', '2'],
train_image: '172.20.32.187/machine-learning/pytorch:pytorch_1.9.1_cuda11.1_detection_aim',
test_dataset: [
{
dataset_id: 20,
dataset_version: 'v0.1.0',
dataset_name: '手写体识别模型依赖测试训练数据集',
},
],
project_dependency: {
url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git',
name: 'somun202304241505581',
branch: 'train_ci_test',
},
parent_models_map: [
{
model_id: 29,
model_version: 'v0.1.0',
model_name: 'mnist模型演化',
},
],
parent_models: [
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.1.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: null,
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
],
children_models: [
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.3.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.31.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.4.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.6.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.7.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.5.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.10.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
{
current_model_id: 29,
exp_ins_id: null,
version: 'v0.11.0',
ref_item: null,
train_task: {},
train_dataset: [],
train_params: [],
train_image: null,
test_dataset: [],
project_dependency: {},
parent_models_map: [],
parent_models: [],
children_models: [],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
],
workflow_id: null,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172558449/mnist_epoch1_0.00.pkl',
file_name: 'mnist_epoch1_0.00.pkl',
file_size: '176.63 KB',
create_by: 'admin',
create_time: '2024-06-12T06:09:56.000+00:00',
},
},
],
workflow_id: 144,
model_version_dependcy_vo: {
name: 'mnist模型演化',
description: '手写体识别模型演化',
available_range: 0,
model_type: '37',
model_tag: '46',
model_type_name: 'PyTorch',
model_tag_name: '图像转文本',
url: 'models/admin/1718172760650/mnist_cnn.pt',
file_name: 'mnist_cnn.pt',
file_size: '176.76 KB',
create_by: 'admin',
create_time: '2024-06-12T06:12:42.000+00:00',
},
},
},
});

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

@@ -33,7 +33,7 @@
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"test": "jest",


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

@@ -0,0 +1,19 @@
.kf-label-value {
display: flex;
align-items: flex-start;
font-size: 16px;
line-height: 1.6;

&__label {
flex: none;
width: 80px;
color: @text-color-secondary;
}

&__value {
flex: 1;
color: @text-color;
white-space: pre-line;
word-break: break-all;
}
}

+ 20
- 0
react-ui/src/components/LabelValue/index.tsx View File

@@ -0,0 +1,20 @@
import classNames from 'classnames';
import './index.less';

type labelValueProps = {
label: string;
value?: any;
className?: string;
style?: React.CSSProperties;
};

function LabelValue({ label, value, className, style }: labelValueProps) {
return (
<div className={classNames('kf-label-value', className)} style={style}>
<div className="kf-label-value__label">{label}</div>
<div className="kf-label-value__value">{value ?? '--'}</div>
</div>
);
}

export default LabelValue;

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

@@ -1,4 +1,4 @@
.modal-title {
.kf-modal-title {
display: flex;
align-items: center;
color: @primary-color;


+ 3
- 3
react-ui/src/components/ModalTitle/index.tsx View File

@@ -6,7 +6,7 @@

import classNames from 'classnames';
import React from 'react';
import styles from './index.less';
import './index.less';

type ModalTitleProps = {
title: React.ReactNode;
@@ -17,8 +17,8 @@ type ModalTitleProps = {

function ModalTitle({ title, image, style, className }: ModalTitleProps) {
return (
<div className={classNames(styles['modal-title'], className)} style={style}>
{image && <img className={styles['modal-title__image']} src={image} alt="" />}
<div className={classNames('kf-modal-title', className)} style={style}>
{image && <img className={'kf-modal-title__image'} src={image} alt="" />}
{title}
</div>
);


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

@@ -17,10 +17,10 @@ const filterResourceStandard: SelectProps<string, ComputingResource>['filterOpti
const convertId = (item: any) => ({ ...item, id: String(item.id) });

export type SelectPropsConfig = {
getOptions: () => Promise<any>;
fieldNames?: SelectProps['fieldNames'];
optionFilterProp?: SelectProps['optionFilterProp'];
filterOption?: SelectProps['filterOption'];
getOptions: () => Promise<any>; // 获取下拉数据
fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
filterOption?: SelectProps['filterOption']; // 过滤函数
};

export const paramSelectConfig: Record<string, SelectPropsConfig> = {


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

@@ -1,7 +1,7 @@
import { getSessionStorageItem, removeSessionStorageItem } from '@/utils/sessionStorage';
import { useEffect, useState } from 'react';

// 获取缓存数据
// 读取缓存数据,组件卸载时清除缓存
export function useSessionStorage<T>(key: string, isObject: boolean, initialValue: T) {
const [storage, setStorage] = useState<T>(initialValue);



+ 1
- 1
react-ui/src/iconfont/iconfont.js
File diff suppressed because it is too large
View File


+ 16
- 13
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -1,3 +1,4 @@
import KFIcon from '@/components/KFIcon';
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max';
@@ -7,21 +8,21 @@ import { ResourceData, ResourceType, resourceConfig } from '../../config';
import ResourceVersion from '../ResourceVersion';
import styles from './index.less';

export enum ResourceInfoTabKeys {
Introduction = 'introduction',
Version = 'version',
Evolution = 'evolution',
}

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';
const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction;
let versionParam = searchParams.get('version');
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined);
@@ -74,8 +75,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {

const items = [
{
key: TabKeys.Introduction,
key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: (
<>
<div className={styles['resource-intro__title']}>简介</div>
@@ -84,8 +86,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
),
},
{
key: TabKeys.Version,
key: ResourceInfoTabKeys.Version,
label: `${typeName}文件/版本`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: (
<ResourceVersion
resourceType={resourceType}
@@ -94,7 +97,7 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
isPublic={info.available_range === 1}
versionList={versionList}
version={version}
isActive={activeTab === TabKeys.Version}
isActive={activeTab === ResourceInfoTabKeys.Version}
getVersionList={getVersionList}
onVersionChange={handleVersionChange}
></ResourceVersion>
@@ -104,15 +107,15 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {

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


+ 22
- 22
react-ui/src/pages/Dataset/config.tsx View File

@@ -24,32 +24,32 @@ export enum ResourceType {
}

type ResourceTypeInfo = {
getList: (params: any) => Promise<any>;
getVersions: (params: any) => Promise<any>;
getFiles: (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;
typeParamKey: string;
tagParamKey: string;
fileReqParamKey: 'models_id' | 'dataset_id';
tabItems: TabsProps['items'];
typeTitle: string;
tagTitle: string;
getList: (params: any) => Promise<any>; // 获取资源列表
getVersions: (params: any) => Promise<any>; // 获取版本列表
getFiles: (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; // 名称
typeParamKey: string; // 类型参数名称,获取资源列表接口使用
tagParamKey: string; // 标签参数名称,获取资源列表接口使用
fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用
tabItems: TabsProps['items']; // tab 列表
typeTitle: string; // 类型标题
tagTitle: string; // 标签标题
typeValue: number; // 从 getAssetIcon 接口获取特定值的数据为 type 分类 (category_id === typeValue)
tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue)
prefix: string; // 前缀
prefix: string; // 图片资源、详情 url 的前缀
deleteModalTitle: string; // 删除弹框的title
addBtnTitle: string; // 新增按钮的title
idParamKey: 'models_id' | 'dataset_id';
uploadAction: string;
uploadAccept?: string;
downloadAllAction: string;
downloadSingleAction: string;
infoTypePropertyName: string;
infoTagPropertyName: string;
idParamKey: 'models_id' | 'dataset_id'; // 新建版本、删除版本接口,版本 id 的参数名称
uploadAction: string; // 上传接口 url
uploadAccept?: string; // 上传文件类型
downloadAllAction: string; // 批量下载接口 url
downloadSingleAction: string; // 单个下载接口 url
infoTypePropertyName: string; // 详情数据中,类型属性名称
infoTagPropertyName: string; // 详情数据中,标签属性名称
};

export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {


+ 8
- 14
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -27,14 +27,12 @@ type Log = {
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 });
}
const optons: ScrollToOptions = {
top: element.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
};

element.scrollTo(optons);
}
};

@@ -153,13 +151,9 @@ function LogGroup({
)}
<div className={styles['log-group__more-button']}>
{showMoreBtn && (
<Button
type="text"
style={{ color: 'white' }}
icon={<DoubleRightOutlined rotate={90} />}
onClick={loadMore}
>
<Button type="text" style={{ color: 'white' }} onClick={loadMore}>
更多
<DoubleRightOutlined rotate={90} />
</Button>
)}
</div>


react-ui/src/pages/Model/components/GraphLegand/index.less → react-ui/src/pages/Model/components/GraphLegend/index.less View File


react-ui/src/pages/Model/components/GraphLegand/index.tsx → react-ui/src/pages/Model/components/GraphLegend/index.tsx View File

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

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

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

function GraphLegand({ style }: GraphLegandProps) {
const legends: GraphLegandData[] = [
function GraphLegend({ style }: GraphLegendProps) {
const legends: GraphLegendData[] = [
{
name: '父模型',
color: '#76b1ff',
color: 'linear-gradient(305deg,#43c9b1 0%,#93dfd1 100%)',
radius: 2,
fill: true,
},
{
name: '当前模型',
color: '#1664ff',
color: 'linear-gradient(139.97deg,#72a1ff 0%,#1664ff 100%)',
radius: 2,
fill: true,
},
{
name: '衍生模型',
color: '#b7cfff',
color: 'linear-gradient(139.97deg,#72b4ff 0%,#169aff 100%)',
radius: 2,
fill: true,
},
{
name: '训练数据集',
color: '#a5d878',
radius: '50%',
fill: true,
},
{
name: '测试数据集',
color: '#d8b578',
radius: '50%',
fill: true,
},
{
name: '项目',
color: 'linear-gradient(305deg,#8981ff 0%,#b3a9ff 100%)',
radius: 6,
fill: true,
},
];
return (
<Flex align="center" className={styles['graph-legend']} style={style}>
@@ -42,7 +60,7 @@ function GraphLegand({ style }: GraphLegandProps) {
width: '16px',
height: '12px',
borderRadius: item.radius,
backgroundColor: item.color,
background: item.color,
}}
></div>
<div className={styles['graph-legend__item__name']}>{item.name}</div>
@@ -52,4 +70,4 @@ function GraphLegand({ style }: GraphLegandProps) {
);
}

export default GraphLegand;
export default GraphLegend;

+ 25
- 277
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -2,268 +2,20 @@ 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';
import G6, { G6GraphEvent, Graph } from '@antv/g6';
// @ts-ignore
import Hierarchy from '@antv/hierarchy';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro';
import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react';
import GraphLegand from '../GraphLegand';
import GraphLegend from '../GraphLegend';
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 };
}
import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils';
import { NodeType, getGraphData, nodeHeight, nodeWidth, normalizeTreeData } from './utils';

type modeModelEvolutionProps = {
resourceId: number;
resourceName: string;
versionList: ResourceVersionData[];
version?: string;
isActive: boolean;
@@ -273,7 +25,6 @@ type modeModelEvolutionProps = {
let graph: Graph;
function ModelEvolution({
resourceId,
resourceName,
versionList,
version,
isActive,
@@ -284,20 +35,22 @@ function ModelEvolution({
const [enterTooltip, setEnterTooltip] = useState(false);
const [nodeTooltipX, setNodeToolTipX] = useState(0);
const [nodeTooltipY, setNodeToolTipY] = useState(0);
const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined);
const [hoverNodeData, setHoverNodeData] = useState<
ModelDepsData | ProjectDependency | TrainDataset | undefined
>(undefined);

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

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

@@ -344,6 +97,7 @@ function ModelEvolution({
fill: '#ffffff',
fontSize: 8,
textAlign: 'center',
cursor: 'pointer',
},
},
},
@@ -383,27 +137,21 @@ function ModelEvolution({
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 { x, y } = model;
const point = graph.getCanvasByPoint(x!, y!);
const zoom = graph.getZoom();
// 更加缩放,调整 tooltip 位置
const offsetX = (nodeWidth * zoom) / 4;
const offsetY = (nodeHeight * zoom) / 4;

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);
setNodeToolTipX(point.x + offsetX);
setNodeToolTipY(graphRef.current!.clientHeight - point.y + offsetY);
setShowNodeTooltip(true);
});

@@ -423,7 +171,7 @@ function ModelEvolution({
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}`;
url = `${origin}/dataset/model/${current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${version}`;
break;
}
case NodeType.project: {
@@ -434,7 +182,7 @@ function ModelEvolution({
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}`;
url = `${origin}/dataset/dataset/${dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${dataset_version}`;
break;
}
default:
@@ -464,12 +212,12 @@ function ModelEvolution({
// 获取模型依赖
const getModelAtlas = async () => {
const params = {
model_id: resourceId,
current_model_id: resourceId,
version,
};
const [res] = await to(getModelAtlasReq(params));
if (res && res.data) {
const data = normalizeTreeData(res.data, resourceName);
const data = normalizeTreeData(res.data);
const graphData = getGraphData(data);

graph.data(graphData);
@@ -502,7 +250,7 @@ function ModelEvolution({
onChange={onVersionChange}
options={versionList}
/>
<GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand>
<GraphLegend style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegend>
</Flex>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
{(showNodeTooltip || enterTooltip) && (


+ 328
- 0
react-ui/src/pages/Model/components/ModelEvolution/utils.tsx View File

@@ -0,0 +1,328 @@
import { changePropertyName, fittingString } from '@/utils';
import { EdgeConfig, GraphData, LayoutConfig, NodeConfig, TreeGraphData, Util } from '@antv/g6';
// @ts-ignore
import Hierarchy from '@antv/hierarchy';

export const nodeWidth = 110;
export const nodeHeight = 50;
export const vGap = nodeHeight + 20;
export const hGap = nodeHeight + 20;
export const ellipseWidth = nodeWidth;

// 数据集节点
const datasetNodes: NodeConfig[] = [];

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

export type Rect = {
x: number; // 矩形中心的 x 坐标
y: number; // 矩形中心的 y 坐标
width: number;
height: number;
};

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

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

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

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

export 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[];
}

// 规范化子数据
export 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
export function getLabel(node: ModelDepsData | ModelDepsAPIData) {
return (
fittingString(`${node.model_version_dependcy_vo.name ?? ''}`, nodeWidth - 12, 8) +
'\n' +
fittingString(`${node.version}`, nodeWidth - 12, 8)
);
}

// 获取 style
export function getStyle(model_type: NodeType) {
let fill = '';
switch (model_type) {
case NodeType.current:
fill = 'l(0) 0:#72a1ff 1:#1664ff';
break;
case NodeType.parent:
fill = 'l(0) 0:#93dfd1 1:#43c9b1';
break;
case NodeType.children:
fill = 'l(0) 0:#72b4ff 1:#169aff';
break;
case NodeType.project:
fill = 'l(0) 0:#b3a9ff 1:#8981ff';
break;
case NodeType.trainDataset:
fill = '#a5d878';
break;
case NodeType.testDataset:
fill = '#d8b578';
break;
default:
break;
}
return {
fill,
};
}

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

// 设置当前模型的数据
normalizedData.model_type = NodeType.current;
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 的数据
export 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;
// 当前模型显示数据集和项目
if (data.model_type === NodeType.current) {
addDatasetDependency(data, node, nodes, edges);
addProjectDependency(data, node, nodes, edges);
} else if (data.model_type === NodeType.children) {
adjustDatasetPosition(node);
}
nodes.push({
...data,
x: node.x,
y: node.y,
});
if (parent) {
edges.push({
source: parent.id,
target: node.id,
});
}
});
return { nodes, edges };
}

// 将数据集转换成 G6 的数据
const addDatasetDependency = (
data: ModelDepsData,
currentNode: NodeConfig,
nodes: NodeConfig[],
edges: EdgeConfig[],
) => {
const { train_dataset, test_dataset } = data;
train_dataset.forEach((item) => {
item.id = `$DTrain_${item.dataset_id}_${item.dataset_version}`;
item.model_type = NodeType.trainDataset;
item.style = getStyle(NodeType.trainDataset);
});
test_dataset.forEach((item) => {
item.id = `$DTest_${item.dataset_id}_${item.dataset_version}`;
item.model_type = NodeType.testDataset;
item.style = getStyle(NodeType.testDataset);
});

datasetNodes.length = 0;
const len = train_dataset.length + test_dataset.length;
[...train_dataset, ...test_dataset].forEach((item, index) => {
const node = { ...item };
node.type = 'ellipse';
node.size = [ellipseWidth, nodeHeight];
node.label =
fittingString(node.dataset_name, ellipseWidth - 12, 8) +
'\n' +
fittingString(node.dataset_version, ellipseWidth - 12, 8);

const half = len / 2 - 0.5;
node.x = currentNode.x! - (half - index) * (ellipseWidth + hGap / 2);
node.y = currentNode.y! - nodeHeight - vGap;
nodes.push(node);
datasetNodes.push(node);
edges.push({
source: currentNode.id,
target: node.id,
sourceAnchor: 2,
targetAnchor: 3,
type: 'cubic-vertical',
});
});
};

// 将模型依赖数据转换成 G6 的数据
const addProjectDependency = (
data: ModelDepsData,
currentNode: NodeConfig,
nodes: NodeConfig[],
edges: EdgeConfig[],
) => {
const { project_dependency } = data;
if (project_dependency?.url) {
const node = { ...project_dependency };
node.id = `$P_${node.url}_${node.branch}`;
node.model_type = NodeType.project;
node.type = 'rect';
node.label = fittingString(node.name, nodeWidth - 12, 8);
node.style = getStyle(NodeType.project);
node.style.radius = nodeHeight / 2;
node.x = currentNode.x;
node.y = currentNode.y! + nodeHeight + vGap;

nodes.push(node);
edges.push({
source: currentNode.id,
target: node.id,
sourceAnchor: 3,
targetAnchor: 2,
type: 'cubic-vertical',
});
}
};

// 判断两个矩形是否相交
function isRectanglesOverlap(rect1: Rect, rect2: Rect) {
const a2x = rect1.x + rect1.width / 2;
const a2y = rect1.y + rect1.height / 2;
const b1x = rect2.x - rect2.width / 2;
const b1y = rect2.y - rect2.height / 2;
return b1y <= a2y && b1x <= a2x;
}

// 判断子节点是否与数据集节点重叠
function isChildrenOverlapDataset(nodes: NodeConfig[], childrenRect: Rect) {
for (const node of nodes) {
const rect = { x: node.x!, y: node.y!, width: nodeWidth, height: nodeHeight };
if (isRectanglesOverlap(rect, childrenRect)) {
return childrenRect;
}
}

return null;
}

// 调整数据集位置
function adjustDatasetPosition(node: NodeConfig) {
const nodeRect = {
x: node.x!,
y: node.y!,
width: nodeWidth,
height: nodeHeight,
};
const overlapRect = isChildrenOverlapDataset(datasetNodes, nodeRect);
if (overlapRect) {
console.log(node);

const adjustRect = {
x: overlapRect.x - nodeWidth - hGap / 2,
y: overlapRect.y,
width: overlapRect.width,
height: overlapRect.height,
};
const lastNode = datasetNodes[datasetNodes.length - 1] as NodeConfig;
const distance = lastNode.x! - adjustRect.x;
datasetNodes.forEach((item) => {
item.x = item.x! - distance;
});
}
}

+ 3
- 4
react-ui/src/pages/Model/components/NodeTooltips/index.less View File

@@ -1,6 +1,6 @@
.node-tooltips {
position: absolute;
top: -100px;
bottom: -100px;
left: -300px;
width: 300px;
padding: 10px;
@@ -18,7 +18,7 @@

&__row {
display: flex;
align-items: center;
align-items: flex-start;
margin: 4px 0;
color: @text-color;
font-size: 14px;
@@ -43,14 +43,13 @@
min-width: 0;
color: @text-color;
font-weight: 500;
.singleLine();
word-break: break-all;
}

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

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

@@ -1,16 +1,16 @@
import { formatDate } from '@/utils/date';
import { ModelDepsData } from '../ModelEvolution';
import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils';
import styles from './index.less';

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

function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) {
function ModelInfo({ data }: { data: ModelDepsData }) {
const gotoExperimentPage = () => {
if (data.train_task?.ins_id) {
const { origin } = location;
@@ -19,21 +19,18 @@ function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsPr
};

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>
<span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo.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>
<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>
@@ -68,9 +65,76 @@ function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsPr
<a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}>
{data.train_task?.name}
</a>
) : null}
) : (
'--'
)}
</div>
</div>
</>
);
}

function DatasetInfo({ data }: { data: TrainDataset }) {
return (
<>
<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.dataset_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.dataset_version || '--'}
</span>
</div>
</div>
</>
);
}

function ProjectInfo({ data }: { data: ProjectDependency }) {
return (
<>
<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.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.branch || '--'}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>项目地址:</span>
<span className={styles['node-tooltips__row__value']}>{data.url || '--'}</span>
</div>
</div>
</>
);
}

function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) {
if (!data) return null;
let Component;
const { model_type } = data;
if (model_type === NodeType.testDataset || model_type === NodeType.trainDataset) {
Component = <DatasetInfo data={data as TrainDataset} />;
} else if (model_type === NodeType.project) {
Component = <ProjectInfo data={data as ProjectDependency} />;
} else {
Component = <ModelInfo data={data as ModelDepsData} />;
}
return (
<div
className={styles['node-tooltips']}
style={{ left: `${x}px`, bottom: `${y}px` }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{Component}
</div>
);
}


+ 20
- 27
react-ui/src/pages/ModelDeployment/Info/index.less View File

@@ -9,37 +9,30 @@
padding: 30px 30px 0;
background-color: white;
border-radius: 10px;
}

&__basic {
&__item {
display: flex;
align-items: flex-start;
font-size: 16px;
line-height: 1.6;
&__tabs {
flex: 1;
min-height: 0;
margin-top: 20px;
padding-bottom: 10px;

.label {
flex: none;
width: 80px;
color: @text-color-secondary;
}
:global {
.ant-tabs {
height: 100%;

.ant-tabs-nav {
margin-bottom: 10px;
}

.ant-tabs-content {
height: 100%;

.value {
flex: 1;
color: @text-color;
white-space: pre-line;
word-break: break-all;
.ant-tabs-tabpane {
height: 100%;
}
}
}
}
}
}

&__guide {
flex: 1;
margin-top: 10px;
padding: 10px;
overflow-y: auto;
color: white;
white-space: pre-wrap;
background-color: rgba(0, 0, 0, 0.85);
}
}

+ 27
- 169
react-ui/src/pages/ModelDeployment/Info/index.tsx View File

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

@@ -25,24 +22,6 @@ export enum ModelDeploymentTabKey {
Log = 'Log',
}

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

function ModelDeploymentInfo() {
const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict);
const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>(
@@ -50,38 +29,32 @@ function ModelDeploymentInfo() {
true,
undefined,
);
const getResourceDescription = useComputingResource()[2];
const [docs, setDocs] = useState('');

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));
}
};
const tabItems = [
{
key: ModelDeploymentTabKey.Predict,
label: '预测',
icon: <KFIcon type="icon-yuce" />,
},
{
key: ModelDeploymentTabKey.Guide,
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
children: <UserGuide info={modelDeployementInfo}></UserGuide>,
},
{
key: ModelDeploymentTabKey.Log,
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
children: <ServerLog info={modelDeployementInfo}></ServerLog>,
},
];

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

// 格式化环境变量
const formatEnvText = () => {
if (!modelDeployementInfo?.env) {
return '--';
}
const env = modelDeployementInfo.env;
return Object.entries(env)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
};

return (
<div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle>
@@ -91,125 +64,10 @@ function ModelDeploymentInfo() {
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>
</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} 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>
<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>
<BasicInfo info={modelDeployementInfo} />
<div className={styles['model-deployment-info__content__tabs']}>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</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>
);


+ 91
- 0
react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx View File

@@ -0,0 +1,91 @@
import LabelValue from '@/components/LabelValue';
import { useComputingResource } from '@/hooks/resource';
import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
import { formatDate } from '@/utils/date';
import { Col, Row } from 'antd';
import ModelDeploymentStatusCell from '../ModelDeployStatusCell';

type BasicInfoProps = {
info?: ModelDeploymentData;
};

function BasicInfo({ info }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[2];

// 格式化环境变量
const formatEnvText = () => {
if (!info?.env) {
return '--';
}
const env = info.env;
return Object.entries(env)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
};

return (
<div>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="服务名称:" value={info?.service_name}></LabelValue>
</Col>
<Col span={10}>
<LabelValue label="镜  像:" value={info?.image}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue
label="状  态:"
value={ModelDeploymentStatusCell(info?.status)}
></LabelValue>
</Col>
<Col span={10}>
<LabelValue label="模  型:" value={info?.model?.show_value}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="创建人:" value={info?.created_by}></LabelValue>
</Col>
<Col span={10}>
<LabelValue label="挂载路径:" value={info?.model_path}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="API URL:" value={info?.url}></LabelValue>
</Col>
<Col span={10}>
<LabelValue label="副本数量:" value={info?.replicas}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="创建时间:" value={formatDate(info?.create_time)}></LabelValue>
</Col>
<Col span={10}>
<LabelValue label="更新时间:" value={formatDate(info?.update_time)}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="环境变量:" value={formatEnvText()}></LabelValue>
</Col>
<Col span={10}>
<LabelValue
label="资源规格:"
value={info?.resource ? getResourceDescription(info.resource) : '--'}
></LabelValue>
</Col>
</Row>
<Row gutter={40}>
<Col span={18}>
<LabelValue label="描  述:" value={info?.description}></LabelValue>
</Col>
</Row>
</div>
);
}

export default BasicInfo;

+ 16
- 0
react-ui/src/pages/ModelDeployment/components/ServerLog/index.less View File

@@ -0,0 +1,16 @@
.server-log {
height: 100%;
&__data {
height: calc(100% - 42px);
margin-top: 10px;
padding: 10px;
overflow-y: auto;
color: white;
white-space: pre-wrap;
background-color: rgba(0, 0, 0, 0.85);

&__more {
padding: 10px 0;
}
}
}

+ 147
- 0
react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx View File

@@ -0,0 +1,147 @@
import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
import { getModelDeploymentLogReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { DoubleRightOutlined } from '@ant-design/icons';
import { Button, DatePicker, type TimeRangePickerProps } from 'antd';
import dayjs from 'dayjs';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import styles from './index.less';
const { RangePicker } = DatePicker;

// 滚动到底部
// const scrollToBottom = (smooth: boolean = true) => {
// const element = document.getElementById('server-log');
// if (element) {
// const optons: ScrollToOptions = {
// top: element.scrollHeight,
// behavior: smooth ? 'smooth' : 'instant',
// };
// element.scrollTo(optons);
// }
// };

type LogData = {
log_content: string;
end_time: string;
start_time: string;
};

type ServerLogProps = {
info?: ModelDeploymentData;
};

function ServerLog({ info }: ServerLogProps) {
const [dateRange, setDateRange] = useState<NonNullable<TimeRangePickerProps['value']>>([
dayjs().add(-1, 'h'),
dayjs(),
]);
const [logTime, setLogTime] = useState<[string, string]>([
`${dateRange[0]!.valueOf() * Math.pow(10, 6)}`,
`${dateRange[1]!.valueOf() * Math.pow(10, 6)}`,
]);
const [logData, setLogData] = useState<LogData[]>([]);
const [hasMore, setHasMore] = useState(false);

const rangePresets: TimeRangePickerProps['presets'] = [
{ label: '最近 1 小时', value: [dayjs().add(-1, 'h'), dayjs()] },
{ label: '最近 2 小时', value: [dayjs().add(-2, 'h'), dayjs()] },
{ label: '最近 3 小时', value: [dayjs().add(-3, 'h'), dayjs()] },
{ label: '最近 1 天', value: [dayjs().add(-1, 'd'), dayjs()] },
{ label: '最近 2 天', value: [dayjs().add(-2, 'd'), dayjs()] },
{ label: '最近 7 天', value: [dayjs().add(-7, 'd'), dayjs()] },
{ label: '最近 14 天', value: [dayjs().add(-14, 'd'), dayjs()] },
{ label: '最近 30 天', value: [dayjs().add(-30, 'd'), dayjs()] },
];

useEffect(() => {
getModelDeploymentLog();
}, [info, logTime]);

// 获取模型部署日志
const getModelDeploymentLog = async () => {
if (info && logTime && logTime.length === 2) {
const params = {
start_time: logTime[0],
end_time: logTime[1],
...pick(info, ['service_id', 'service_ins_id']),
};
const [res] = await to(getModelDeploymentLogReq(params));
if (res && res.data) {
setLogData((prev) => [...prev, res.data]);
setHasMore(!!res.data.log_content);
// setTimeout(() => {
// scrollToBottom();
// }, 100);
}
}
};

// 搜索
const handleSearch = () => {
setLogData([]);
setHasMore(false);
setLogTime([
`${dateRange[0]!.valueOf() * Math.pow(10, 6)}`,
`${dateRange[1]!.valueOf() * Math.pow(10, 6)}`,
]);
};

// 加载更多日志
const loadMoreLog = () => {
const lastLog = logData[logData.length - 1];
setLogTime([lastLog.start_time, lastLog.end_time]);
};

// 禁止选择今天之后和之前31天的日期
const disabledDate: TimeRangePickerProps['disabledDate'] = (currentDate) => {
return (
Date.now() - currentDate.valueOf() < 0 ||
Date.now() - currentDate.valueOf() > 31 * 24 * 60 * 60 * 1000
);
};

// 处理日期变化
const handleRangeChange: TimeRangePickerProps['onChange'] = (dates) => {
if (dates) {
setDateRange(dates);
}
};

return (
<div className={styles['server-log']}>
<div>
<RangePicker
presets={rangePresets}
showTime
value={dateRange}
format="YYYY-MM-DD HH:mm:ss"
onChange={handleRangeChange}
allowClear={false}
disabledDate={disabledDate}
/>
<Button type="default" style={{ marginLeft: '20px' }} onClick={handleSearch}>
查询
</Button>
</div>
{logData.length > 0 && (
<div className={styles['server-log__data']} id="server-log">
<div>{logData.map((v) => v.log_content).join('') || '暂无日志'}</div>
{hasMore && (
<Button
type="text"
className={styles['server-log__data__more']}
style={{ color: 'white' }}
onClick={loadMoreLog}
>
更多
<DoubleRightOutlined rotate={90} />
</Button>
)}
</div>
)}
</div>
);
}

export default ServerLog;

+ 8
- 0
react-ui/src/pages/ModelDeployment/components/UserGuide/index.less View File

@@ -0,0 +1,8 @@
.user-guide {
height: 100%;
padding: 10px;
overflow-y: auto;
color: white;
white-space: pre-wrap;
background-color: rgba(0, 0, 0, 0.85);
}

+ 32
- 0
react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx View File

@@ -0,0 +1,32 @@
import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
import { getModelDeploymentDocsReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import styles from './index.less';

type UserGuideProps = {
info?: ModelDeploymentData;
};

function UserGuide({ info }: UserGuideProps) {
const [docs, setDocs] = useState('');
useEffect(() => {
getModelDeploymentDocs();
}, [info]);

// 获取模型部署文档
const getModelDeploymentDocs = async () => {
if (info) {
const params = pick(info, ['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));
}
}
};

return <div className={styles['user-guide']}>{docs}</div>;
}

export default UserGuide;

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

@@ -27,16 +27,16 @@ export type MirrorVersion = {
};

export type SelectorTypeInfo = {
getList: (params: any) => Promise<any>;
getVersions: (params: any) => Promise<any>;
getFiles: (params: any) => Promise<any>;
handleVersionResponse: (res: any) => any[];
modalIcon: string;
buttonIcon: string;
name: string;
litReqParamKey: 'available_range' | 'image_type';
fileReqParamKey: 'models_id' | 'dataset_id';
tabItems: TabsProps['items'];
getList: (params: any) => Promise<any>; // 获取资源列表
getVersions: (params: any) => Promise<any>; // 获取资源版本列表
getFiles: (params: any) => Promise<any>; // 获取资源文件列表
handleVersionResponse: (res: any) => any[]; // 处理版本列表接口数据
modalIcon: string; // modal icon
buttonIcon: string; // button icon
name: string; // 名称
litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用
fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用
tabItems: TabsProps['items']; // tab 列表
};

// 获取镜像文件列表,为了兼容数据集和模型


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

@@ -35,13 +35,11 @@ const EditPipeline = () => {
}, []);

const onDragEnd = (val) => {
console.log(val);
const _x = val.x;
const _y = val.y;
const point = graph.getPointByClient(_x, _y);
let model = {};
// 元模型
model = {
const model = {
...val,
x: point.x,
y: point.y,


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

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

// 获取模型部署日志
export function getModelDeploymentLogReq(data: any) {
return request(`/api/v1/model/getAppLog`, {
method: 'POST',
data,
});
}

+ 22
- 1
react-ui/src/utils/ui.tsx View File

@@ -71,7 +71,13 @@ export const gotoLoginPage = (toHome: boolean = true) => {
}
};

// 上传文件校验
/**
* 验证文件上传
*
* @param {UploadFile[]} files - The array of uploaded files.
* @param {boolean} [required=true] - Flag indicating if files are required.
* @return {boolean} Returns true if all files are valid, false otherwise.
*/
export const validateUploadFiles = (files: UploadFile[], required: boolean = true): boolean => {
if (required && files.length === 0) {
message.error('请上传文件');
@@ -95,3 +101,18 @@ export const validateUploadFiles = (files: UploadFile[], required: boolean = tru
});
return !hasError;
};

/**
* 滚动到底部
*
* @param {boolean} smooth - Determines if the scroll should be smooth
*/
export const scrollToBottom = (element: HTMLElement | null, smooth: boolean = true) => {
if (element) {
const optons: ScrollToOptions = {
top: element.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
};
element.scrollTo(optons);
}
};

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

@@ -167,8 +167,6 @@ public class ModelDependencyServiceImpl implements ModelDependencyService {
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());
@@ -182,9 +180,16 @@ public class ModelDependencyServiceImpl implements ModelDependencyService {
modelVersionDependcyVo.setUrl(modelsVersion.getUrl());
modelVersionDependcyVo.setCreateBy(modelsVersion.getCreateBy());
modelVersionDependcyVo.setCreateTime(modelsVersion.getCreateTime());
modelDependcyTreeVo.setWorkflowId(experiment.getWorkflowId());
modelDependcyTreeVo.setModelVersionDependcyVo(modelVersionDependcyVo);
ExperimentIns experimentIns = experimentInsService.queryById(expInsId);
if (experimentIns == null){
return modelDependcyTreeVo;
}
Experiment experiment = experimentService.queryById(experimentIns.getExperimentId());
if (experiment == null){
return modelDependcyTreeVo;
}
modelDependcyTreeVo.setWorkflowId(experiment.getWorkflowId());
return modelDependcyTreeVo;
}



+ 1
- 1
ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ModelDependencyDaoMapper.xml View File

@@ -27,7 +27,7 @@
id,current_model_id,exp_ins_id,parent_models,ref_item,train_task,train_dataset,train_params,train_image,test_dataset,project_dependency,version,create_by,create_time,update_by,update_time,state
from model_dependency
<where>
parent_models like concat('%', #{model_id}, '%') AND parent_models like concat('%', #{version}, '%')
parent_models like concat('%', #{model_id}, '%') AND parent_models like concat('%', #{version}, '%') and state = 1
</where>
</select>



Loading…
Cancel
Save