Browse Source

Merge pull request 'feat: 模型演化添加数据集legend' (#62) from dev-zw into dev

pull/63/head
cp3hnu 1 year ago
parent
commit
8a5bad70c3
27 changed files with 878 additions and 554 deletions
  1. +19
    -0
      react-ui/src/components/LabelValue/index.less
  2. +20
    -0
      react-ui/src/components/LabelValue/index.tsx
  3. +1
    -1
      react-ui/src/components/ModalTitle/index.less
  4. +3
    -3
      react-ui/src/components/ModalTitle/index.tsx
  5. +4
    -4
      react-ui/src/components/ParameterSelect/config.tsx
  6. +1
    -1
      react-ui/src/hooks/sessionStorage.ts
  7. +1
    -1
      react-ui/src/iconfont/iconfont.js
  8. +16
    -12
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  9. +22
    -22
      react-ui/src/pages/Dataset/config.tsx
  10. +8
    -14
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  11. +0
    -0
      react-ui/src/pages/Model/components/GraphLegend/index.less
  12. +28
    -10
      react-ui/src/pages/Model/components/GraphLegend/index.tsx
  13. +27
    -271
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  14. +341
    -0
      react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
  15. +1
    -3
      react-ui/src/pages/Model/components/NodeTooltips/index.less
  16. +4
    -2
      react-ui/src/pages/Model/components/NodeTooltips/index.tsx
  17. +20
    -27
      react-ui/src/pages/ModelDeployment/Info/index.less
  18. +27
    -169
      react-ui/src/pages/ModelDeployment/Info/index.tsx
  19. +91
    -0
      react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
  20. +16
    -0
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.less
  21. +147
    -0
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx
  22. +8
    -0
      react-ui/src/pages/ModelDeployment/components/UserGuide/index.less
  23. +32
    -0
      react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx
  24. +10
    -10
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
  25. +1
    -3
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  26. +8
    -0
      react-ui/src/services/modelDeployment/index.ts
  27. +22
    -1
      react-ui/src/utils/ui.tsx

+ 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; display: flex;
align-items: center; align-items: center;
color: @primary-color; color: @primary-color;


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

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


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


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


function ModalTitle({ title, image, style, className }: ModalTitleProps) { function ModalTitle({ title, image, style, className }: ModalTitleProps) {
return ( 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} {title}
</div> </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) }); const convertId = (item: any) => ({ ...item, id: String(item.id) });


export type SelectPropsConfig = { 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> = { 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 { getSessionStorageItem, removeSessionStorageItem } from '@/utils/sessionStorage';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';


// 获取缓存数据
// 读取缓存数据,组件卸载时清除缓存
export function useSessionStorage<T>(key: string, isObject: boolean, initialValue: T) { export function useSessionStorage<T>(key: string, isObject: boolean, initialValue: T) {
const [storage, setStorage] = useState<T>(initialValue); 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
- 12
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 ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max'; import { useParams, useSearchParams } from '@umijs/max';
@@ -7,21 +8,21 @@ import { ResourceData, ResourceType, resourceConfig } from '../../config';
import ResourceVersion from '../ResourceVersion'; import ResourceVersion from '../ResourceVersion';
import styles from './index.less'; import styles from './index.less';


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

type ResourceIntroProps = { type ResourceIntroProps = {
resourceType: ResourceType; resourceType: ResourceType;
}; };


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

const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const [info, setInfo] = useState<ResourceData>({} as ResourceData); const [info, setInfo] = useState<ResourceData>({} as ResourceData);
const locationParams = useParams(); const locationParams = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const defaultTab = searchParams.get('tab') || '1';
const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction;
let versionParam = searchParams.get('version'); let versionParam = searchParams.get('version');
const [versionList, setVersionList] = useState([]); const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined); const [version, setVersion] = useState<string | undefined>(undefined);
@@ -74,8 +75,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {


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


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


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

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


type ResourceTypeInfo = { 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) typeValue: number; // 从 getAssetIcon 接口获取特定值的数据为 type 分类 (category_id === typeValue)
tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue) tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue)
prefix: string; // 前缀
prefix: string; // 图片资源、详情 url 的前缀
deleteModalTitle: string; // 删除弹框的title deleteModalTitle: string; // 删除弹框的title
addBtnTitle: 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> = { 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 scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0];
if (element) { 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']}> <div className={styles['log-group__more-button']}>
{showMoreBtn && ( {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> </Button>
)} )}
</div> </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 { Flex } from 'antd';
import styles from './index.less'; import styles from './index.less';


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


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


function GraphLegand({ style }: GraphLegandProps) {
const legends: GraphLegandData[] = [
function GraphLegend({ style }: GraphLegendProps) {
const legends: GraphLegendData[] = [
{ {
name: '父模型', name: '父模型',
color: '#76b1ff',
color: 'linear-gradient(305deg,#43c9b1 0%,#93dfd1 100%)',
radius: 2, radius: 2,
fill: true, fill: true,
}, },
{ {
name: '当前模型', name: '当前模型',
color: '#1664ff',
color: 'linear-gradient(139.97deg,#72a1ff 0%,#1664ff 100%)',
radius: 2, radius: 2,
fill: true, fill: true,
}, },
{ {
name: '衍生模型', name: '衍生模型',
color: '#b7cfff',
color: 'linear-gradient(139.97deg,#72b4ff 0%,#169aff 100%)',
radius: 2, radius: 2,
fill: true, 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 ( return (
<Flex align="center" className={styles['graph-legend']} style={style}> <Flex align="center" className={styles['graph-legend']} style={style}>
@@ -42,7 +60,7 @@ function GraphLegand({ style }: GraphLegandProps) {
width: '16px', width: '16px',
height: '12px', height: '12px',
borderRadius: item.radius, borderRadius: item.radius,
backgroundColor: item.color,
background: item.color,
}} }}
></div> ></div>
<div className={styles['graph-legend__item__name']}>{item.name}</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;

+ 27
- 271
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -2,264 +2,17 @@ import { useEffectWhen } from '@/hooks';
import { ResourceVersionData } from '@/pages/Dataset/config'; import { ResourceVersionData } from '@/pages/Dataset/config';
import { getModelAtlasReq } from '@/services/dataset/index.js'; import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { changePropertyName, fittingString } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6, {
EdgeConfig,
G6GraphEvent,
Graph,
GraphData,
LayoutConfig,
NodeConfig,
TreeGraphData,
Util,
} from '@antv/g6';
import G6, { G6GraphEvent, Graph, Item } from '@antv/g6';
// @ts-ignore // @ts-ignore
import Hierarchy from '@antv/hierarchy';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro';
import { Flex, Select } from 'antd'; import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import GraphLegand from '../GraphLegand';
import GraphLegend from '../GraphLegend';
import NodeTooltips from '../NodeTooltips'; import NodeTooltips from '../NodeTooltips';
import styles from './index.less'; 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 = { type modeModelEvolutionProps = {
resourceId: number; resourceId: number;
@@ -288,16 +41,16 @@ function ModelEvolution({


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


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


@@ -361,14 +114,15 @@ function ModelEvolution({
default: [ default: [
'drag-canvas', 'drag-canvas',
'zoom-canvas', 'zoom-canvas',
// {
// type: 'collapse-expand',
// onChange(item?: Item, collapsed?: boolean) {
// const data = item!.getModel();
// data.collapsed = collapsed;
// return true;
// },
// },
'drag-node',
{
type: 'collapse-expand',
onChange(item?: Item, collapsed?: boolean) {
const data = item!.getModel();
data.collapsed = collapsed;
return true;
},
},
], ],
}, },
}); });
@@ -392,17 +146,19 @@ function ModelEvolution({
return; return;
} }
const point = graph.getCanvasByPoint(x!, y!); 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; const canvasWidth = graphRef.current!.clientWidth;
if (point.x + 300 > canvasWidth) { if (point.x + 300 > canvasWidth) {
point.x = canvasWidth - 300; point.x = canvasWidth - 300;
} }
const zoom = graph.getZoom();
// 更加缩放,调整 tooltip 位置
const offsetY = (nodeHeight * zoom) / 4;


setHoverNodeData(model); setHoverNodeData(model);
setNodeToolTipX(point.x);
// 92: 版本选择器的高度,296: tooltip的高度
setNodeToolTipX(point.x + offsetX);
// 92: 版本选择器的高度,296: tooltip 的高度
setNodeToolTipY(point.y + 92 - 296 - offsetY); setNodeToolTipY(point.y + 92 - 296 - offsetY);
setShowNodeTooltip(true); setShowNodeTooltip(true);
}); });
@@ -423,7 +179,7 @@ function ModelEvolution({
case NodeType.children: case NodeType.children:
case NodeType.parent: { case NodeType.parent: {
const { current_model_id, version } = model as ModelDepsData; 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; break;
} }
case NodeType.project: { case NodeType.project: {
@@ -434,7 +190,7 @@ function ModelEvolution({
case NodeType.trainDataset: case NodeType.trainDataset:
case NodeType.testDataset: { case NodeType.testDataset: {
const { dataset_id, dataset_version } = model as TrainDataset; 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; break;
} }
default: default:
@@ -502,7 +258,7 @@ function ModelEvolution({
onChange={onVersionChange} onChange={onVersionChange}
options={versionList} options={versionList}
/> />
<GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand>
<GraphLegend style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegend>
</Flex> </Flex>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
{(showNodeTooltip || enterTooltip) && ( {(showNodeTooltip || enterTooltip) && (


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

@@ -0,0 +1,341 @@
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 datasetRects: Rect[] = [];

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

export type Rect = {
x: number;
y: number;
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,
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);
// let first1 = { ...normalizedData.children[0] };
// let first2 = { ...normalizedData.children[0] };
// let first3 = { ...normalizedData.children[0] };
// first1.current_model_id = 202020;
// first2.current_model_id = 202021;
// first3.current_model_id = 202022;
// normalizedData.children.push(first1, first2, first3);

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;
console.log('data', data);

// 当前模型显示数据集和项目
if (data.model_type === NodeType.current) {
addDatasetDependency(data, node, nodes, edges);
addProjectDependency(data, node, nodes, edges);
} else if (data.model_type === NodeType.children) {
adjustChildrenPosition(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.model_type = NodeType.trainDataset;
item.style = getStyle(NodeType.trainDataset);
});
test_dataset.forEach((item) => {
item.id = `$DTest_${item.dataset_id}`;
item.model_type = NodeType.testDataset;
item.style = getStyle(NodeType.testDataset);
});

datasetRects.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);
node.y = currentNode.y! - nodeHeight - vGap;
nodes.push(node);
edges.push({
source: currentNode.id,
target: node.id,
sourceAnchor: 2,
targetAnchor: 3,
type: 'cubic-vertical',
});
datasetRects.push({
x: node.x - ellipseWidth / 2,
y: node.y - nodeHeight / 2,
width: ellipseWidth,
height: nodeHeight,
});
});
};

// 将模型依赖数据转换成 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.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 isRectanglesIntersect(rect1: Rect, rect2: Rect) {
return !(
rect1.x + rect1.width < rect2.x ||
rect1.x > rect2.x + rect2.width ||
rect1.y + rect1.height < rect2.y ||
rect1.y > rect2.y + rect2.height
);
}

// 判断子节点是否与数据集节点重叠
function isChildrenIntersectDataset(rects: Rect[], childrenRect: Rect) {
for (const r of rects) {
if (isRectanglesIntersect(r, childrenRect)) {
return r;
}
}

return null;
}

// 计算子节点位置
function adjustChildrenPosition(node: NodeConfig) {
const nodeRect = {
x: node.x! - nodeWidth / 2,
y: node.y! - nodeHeight / 2,
width: nodeWidth,
height: nodeHeight,
};
const overlapRect = isChildrenIntersectDataset(datasetRects, nodeRect);
if (overlapRect) {
const offsetY = nodeRect.y - overlapRect.y;
const space = 10; //(vGap + Math.abs(offsetY) - nodeHeight) / 2;
if (offsetY >= 0) {
node.y = node.y! + (nodeHeight - offsetY + space);
} else {
node.y = node.y! - (nodeHeight - Math.abs(offsetY) + space);
}
}
}

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

@@ -18,7 +18,7 @@


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


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

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

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


type NodeTooltipsProps = { type NodeTooltipsProps = {
@@ -68,7 +68,9 @@ function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsPr
<a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}> <a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}>
{data.train_task?.name} {data.train_task?.name}
</a> </a>
) : null}
) : (
'--'
)}
</div> </div>
</div> </div>
</div> </div>


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

@@ -9,37 +9,30 @@
padding: 30px 30px 0; padding: 30px 30px 0;
background-color: white; background-color: white;
border-radius: 10px; 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 KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
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 { to } from '@/utils/promise';
import { modelDeploymentInfoKey } from '@/utils/sessionStorage'; 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 { ModelDeploymentData } from '../types';
import styles from './index.less'; import styles from './index.less';


@@ -25,24 +22,6 @@ export enum ModelDeploymentTabKey {
Log = 'Log', 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() { function ModelDeploymentInfo() {
const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict); const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict);
const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>( const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>(
@@ -50,38 +29,32 @@ function ModelDeploymentInfo() {
true, true,
undefined, 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,重置数据 // 切换 Tab,重置数据
const hanleTabChange: TabsProps['onChange'] = (value) => { const hanleTabChange: TabsProps['onChange'] = (value) => {
setActiveTab(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 ( return (
<div className={styles['model-deployment-info']}> <div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle> <PageTitle title="服务详情"></PageTitle>
@@ -91,125 +64,10 @@ function ModelDeploymentInfo() {
image={require('@/assets/img/mirror-basic.png')} image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }} style={{ marginBottom: '26px' }}
></SubAreaTitle> ></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> </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>
); );


+ 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 = { 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) => { const onDragEnd = (val) => {
console.log(val);
const _x = val.x; const _x = val.x;
const _y = val.y; const _y = val.y;
const point = graph.getPointByClient(_x, _y); const point = graph.getPointByClient(_x, _y);
let model = {};
// 元模型 // 元模型
model = {
const model = {
...val, ...val,
x: point.x, x: point.x,
y: point.y, y: point.y,


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

@@ -67,3 +67,11 @@ export function getModelDeploymentDocsReq(data: any) {
data, 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 => { export const validateUploadFiles = (files: UploadFile[], required: boolean = true): boolean => {
if (required && files.length === 0) { if (required && files.length === 0) {
message.error('请上传文件'); message.error('请上传文件');
@@ -95,3 +101,18 @@ export const validateUploadFiles = (files: UploadFile[], required: boolean = tru
}); });
return !hasError; 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);
}
};

Loading…
Cancel
Save