feat: 完成模型演化

This commit is contained in:
cp3hnu 2024-06-06 11:18:33 +08:00
parent d00bc941c1
commit 53dc404113
20 changed files with 934 additions and 73 deletions

View File

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

View File

@ -2,13 +2,10 @@
height: 100%;
&__top {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 110px;
margin-bottom: 10px;
padding: 25px 30px;
padding: 20px 30px 0;
background-image: url(/assets/images/dataset-back.png);
background-repeat: no-repeat;
background-position: top center;
@ -17,7 +14,7 @@
&__name {
margin-bottom: 12px;
color: @text-color;
font-size: 20;
font-size: 20px;
}
&__tag {
@ -36,6 +33,22 @@
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
:global {
.ant-tabs {
height: 100%;
.ant-tabs-content-holder {
height: 100%;
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
overflow-y: auto;
}
}
}
}
}
}
&__title {

View File

@ -1,3 +1,4 @@
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max';
import { Flex, Tabs } from 'antd';
@ -12,14 +13,19 @@ type ResourceIntroProps = {
const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const [info, setInfo] = useState<ResourceData>({} as ResourceData);
const locationParams = useParams(); //新版本获取路由参数接口
const locationParams = useParams();
const [searchParams] = useSearchParams();
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const isPublic = searchParams.get('isPublic') === 'true';
const defaultTab = searchParams.get('tab') || '1';
let versionParam = searchParams.get('version');
const resourceId = Number(locationParams.id);
const name = resourceConfig[resourceType].name;
const typeName = resourceConfig[resourceType].name; // 数据集/模型
useEffect(() => {
getModelByDetail();
getVersionList();
}, []);
// 获取详情
@ -31,10 +37,39 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
}
};
// 获取版本列表
const getVersionList = async () => {
const request = resourceConfig[resourceType].getVersions;
const [res] = await to(request(resourceId));
if (res && res.data && res.data.length > 0) {
setVersionList(
res.data.map((item: string) => {
return {
label: item,
value: item,
};
}),
);
if (versionParam) {
setVersion(versionParam);
versionParam = null;
} else {
setVersion(res.data[0]);
}
} else {
setVersion(undefined);
}
};
// 版本变化
const handleVersionChange = (value: string) => {
setVersion(value);
};
const items = [
{
key: '1',
label: `${name}简介`,
label: `${typeName}简介`,
children: (
<>
<div className={styles['resource-intro__title']}></div>
@ -44,18 +79,38 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
},
{
key: '2',
label: `${name}文件/版本`,
label: `${typeName}文件/版本`,
children: (
<ResourceVersion
resourceType={resourceType}
resourceId={resourceId}
resourceName={info.name}
isPublic={isPublic}
versionList={versionList}
version={version}
getVersionList={getVersionList}
onVersionChange={handleVersionChange}
></ResourceVersion>
),
},
];
if (resourceType === ResourceType.Model) {
items.push({
key: '3',
label: `模型演化`,
children: (
<ModelEvolution
resourceId={resourceId}
resourceName={info.name}
versionList={versionList}
version={version}
onVersionChange={handleVersionChange}
></ModelEvolution>
),
});
}
const infoTypePropertyName = resourceConfig[resourceType]
.infoTypePropertyName as keyof ResourceData;
const infoTagPropertyName = resourceConfig[resourceType]
@ -64,21 +119,25 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
return (
<div className={styles['resource-intro']}>
<div className={styles['resource-intro__top']}>
<span className={styles['resource-intro__top__name']}>{info.name}</span>
<div className={styles['resource-intro__top__name']}>{info.name}</div>
<Flex align="center">
<div className={styles['resource-intro__top__tag']}>
{name} id{info.id}
</div>
<div className={styles['resource-intro__top__tag']}>
{info[infoTypePropertyName] || '--'}
</div>
<div className={styles['resource-intro__top__tag']}>
{info[infoTagPropertyName] || '--'}
{typeName} id{info.id}
</div>
{info[infoTypePropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTypePropertyName] || '--'}
</div>
)}
{info[infoTagPropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTagPropertyName] || '--'}
</div>
)}
</Flex>
</div>
<div className={styles['resource-intro__bottom']}>
<Tabs defaultActiveKey="1" items={items}></Tabs>
<Tabs defaultActiveKey={defaultTab} items={items}></Tabs>
</div>
</div>
);

View File

@ -2,14 +2,13 @@ import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
import { ResourceType } from '@/pages/Dataset/config';
import { ResourceFileData, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { App, Button, Flex, Select, Table } from 'antd';
import { useEffect, useState } from 'react';
import { ResourceFileData, resourceConfig } from '../../config';
import styles from './index.less';
type ResourceVersionProps = {
@ -17,42 +16,32 @@ type ResourceVersionProps = {
resourceId: number;
resourceName: string;
isPublic: boolean;
versionList: { label: string; value: string }[];
version?: string;
getVersionList: () => void;
onVersionChange: (version: string) => void;
};
function ResourceVersion({
resourceType,
resourceId,
resourceName,
isPublic,
versionList,
version,
getVersionList,
onVersionChange,
}: ResourceVersionProps) {
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const [fileList, setFileList] = useState<ResourceFileData[]>([]);
const { message } = App.useApp();
// 获取版本文件列表
useEffect(() => {
getVersionList();
}, []);
// 获取版本列表
const getVersionList = async () => {
const request = resourceConfig[resourceType].getVersions;
const [res] = await to(request(resourceId));
if (res && res.data && res.data.length > 0) {
setVersionList(
res.data.map((item: string) => {
return {
label: item,
value: item,
};
}),
);
setVersion(res.data[0]);
getFileList(res.data[0]);
if (version) {
getFileList(version);
} else {
setVersion(undefined);
setFileList([]);
}
};
}, [resourceId, version]);
// 获取版本下的文件列表
const getFileList = async (version: string) => {
@ -120,16 +109,6 @@ function ResourceVersion({
downLoadZip(`${url}/${record.id}`);
};
// 版本变化
const handleChange = (value: string) => {
if (value) {
getFileList(value);
setVersion(value);
} else {
setVersion(undefined);
}
};
const columns = [
{
title: '序号',
@ -194,8 +173,7 @@ function ResourceVersion({
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
allowClear
onChange={handleChange}
onChange={onVersionChange}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>

View File

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

View File

@ -0,0 +1,17 @@
.graph-legend {
&__item {
margin-right: 20px;
color: @text-color;
font-size: @font-size-content;
&:last-child {
margin-right: 0;
}
&__name {
margin-left: 10px;
color: @text-color-secondary;
font-size: @font-size-content;
}
}
}

View File

@ -0,0 +1,55 @@
import { Flex } from 'antd';
import styles from './index.less';
type GraphLegandData = {
name: string;
color: string;
radius: number;
fill: boolean;
};
type GraphLegandProps = {
style?: React.CSSProperties;
};
function GraphLegand({ style }: GraphLegandProps) {
const legends: GraphLegandData[] = [
{
name: '父模型',
color: '#76b1ff',
radius: 2,
fill: true,
},
{
name: '当前模型',
color: '#1664ff',
radius: 2,
fill: true,
},
{
name: '衍生模型',
color: '#b7cfff',
radius: 2,
fill: true,
},
];
return (
<Flex align="center" className={styles['graph-legend']} style={style}>
{legends.map((item) => (
<Flex align="center" key={item.name} className={styles['graph-legend__item']}>
<div
style={{
width: '16px',
height: '12px',
borderRadius: item.radius,
backgroundColor: item.color,
}}
></div>
<div className={styles['graph-legend__item__name']}>{item.name}</div>
</Flex>
))}
</Flex>
);
}
export default GraphLegand;

View File

@ -0,0 +1,18 @@
.model-evolution {
width: 100%;
height: 100%;
background-color: white;
&__top {
padding: 30px 0;
color: @text-color;
font-size: @font-size-content;
}
&__graph {
height: calc(100% - 92px);
background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}
}

View File

@ -0,0 +1,484 @@
import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less';
import { changePropertyName, fittingString } from '@/utils';
import { to } from '@/utils/promise';
import G6, {
EdgeConfig,
G6GraphEvent,
Graph,
GraphData,
LayoutConfig,
NodeConfig,
TreeGraphData,
Util,
} from '@antv/g6';
// @ts-ignore
import Hierarchy from '@antv/hierarchy';
import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react';
import GraphLegand from '../GraphLegand';
import NodeTooltips from '../NodeTooltips';
import styles from './index.less';
const nodeWidth = 98;
const nodeHeight = 58;
const vGap = 30;
const hGap = 30;
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;
}
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;
children_models: ModelDepsAPIData[];
parent_models: ModelDepsAPIData[];
}
interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData {
children: ModelDepsData[];
}
// 规范化子数据
function normalizeChildren(data: ModelDepsData[]) {
if (Array.isArray(data)) {
data.forEach((item) => {
item.id = `$M_${item.current_model_id}_${item.version}`;
item.label = getLabel(item);
item.style = getStyle(NodeType.children);
item.model_type = 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 = '#0000ff';
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.label = getLabel(normalizedData);
normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
normalizedData.style = getStyle(NodeType.current);
normalizedData.model_type = NodeType.current;
normalizedData.current_model_name = currentNodeName;
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,
id: `$M_${parent.current_model_id}_${parent.version}`,
model_type: NodeType.parent,
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,
getHGap: () => hGap,
};
// 树形布局计算出坐标
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 + 30);
item.y = node.y! - nodeHeight - 30;
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 + 30;
nodes.push(project_dependency);
edges.push({
source: node.id,
target: project_dependency.id,
sourceAnchor: 3,
targetAnchor: 2,
type: 'cubic-vertical',
});
}
}
});
return { nodes, edges };
}
type modeModelEvolutionProps = {
resourceId: number;
resourceName: string;
versionList: { label: string; value: string }[];
version?: string;
onVersionChange: (version: string) => void;
};
let graph: Graph;
function ModelEvolution({
resourceId,
resourceName,
versionList,
version,
onVersionChange,
}: modeModelEvolutionProps) {
const graphRef = useRef<HTMLDivElement>(null);
const [showNodeTooltip, setShowNodeTooltip] = useState(false);
const [enterTooltip, setEnterTooltip] = useState(false);
const [nodeTooltipX, setNodeToolTipX] = useState(0);
const [nodeTooltipY, setNodeToolTipY] = useState(0);
const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined);
useEffect(() => {
initGraph();
const changSize = () => {
if (!graph || graph.get('destroyed')) return;
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
};
window.addEventListener('resize', changSize);
return () => {
window.removeEventListener('resize', changSize);
};
}, []);
useEffect(() => {
if (version) {
getModelAtlas();
} else {
graph.data({
nodes: [],
edges: [],
});
graph.render();
graph.fitView();
}
}, [resourceId, version]);
// 初始化图
const initGraph = () => {
graph = new G6.Graph({
container: graphRef.current!,
width: graphRef.current!.clientWidth,
height: graphRef.current!.clientHeight,
// animate: false,
fitView: true,
fitViewPadding: [50, 100, 50, 100],
minZoom: 0.5,
maxZoom: 5,
defaultNode: {
type: 'rect',
size: [nodeWidth, nodeHeight],
anchorPoints: [
[0, 0.5],
[1, 0.5],
[0.5, 0],
[0.5, 1],
],
style: {
fill: themes['primaryColor'],
lineWidth: 0,
radius: 6,
cursor: 'pointer',
},
labelCfg: {
position: 'center',
style: {
fill: '#ffffff',
fontSize: 8,
textAlign: 'center',
},
},
},
defaultEdge: {
type: 'cubic-horizontal',
labelCfg: {
autoRotate: true,
},
style: {
stroke: '#a2c1ff',
lineWidth: 1,
},
},
modes: {
default: [
'drag-canvas',
'zoom-canvas',
// {
// type: 'collapse-expand',
// onChange(item?: Item, collapsed?: boolean) {
// const data = item!.getModel();
// data.collapsed = collapsed;
// return true;
// },
// },
],
},
});
bindEvents();
};
// 绑定事件
const bindEvents = () => {
graph.on('node:mouseenter', (e: G6GraphEvent) => {
const nodeItem = e.item;
graph.setItemState(nodeItem, 'hover', true);
const model = nodeItem.getModel() as ModelDepsData;
const { x, y, model_type } = model;
if (
model_type === NodeType.project ||
model_type === NodeType.trainDataset ||
model_type === NodeType.testDataset
) {
return;
}
const point = graph.getCanvasByPoint(x!, y!);
const canvasWidth = graphRef.current!.clientWidth;
if (point.x + 300 > canvasWidth) {
point.x = canvasWidth - 300;
}
setHoverNodeData(model as ModelDepsData);
setNodeToolTipX(point.x);
setNodeToolTipY(point.y - 240);
setShowNodeTooltip(true);
});
graph.on('node:mouseleave', (e: G6GraphEvent) => {
const nodeItem = e.item;
graph.setItemState(nodeItem, 'hover', false);
setShowNodeTooltip(false);
});
graph.on('node:click', (e: G6GraphEvent) => {
const nodeItem = e.item;
const model = nodeItem.getModel() as ModelDepsChildren;
const { model_type } = model;
const { origin } = location;
let url: string = '';
switch (model_type) {
case NodeType.children:
case NodeType.current:
case NodeType.parent: {
const { current_model_id, version } = model as ModelDepsData;
url = `${origin}/dataset/model/${current_model_id}?isPublic=true&tab=3&version=${version}`;
break;
}
case NodeType.project: {
const { url: projectUrl } = model as ProjectDependency;
url = projectUrl;
break;
}
case NodeType.trainDataset:
case NodeType.testDataset: {
const { dataset_id, dataset_version } = model as TrainDataset;
url = `${origin}/dataset/dataset/${dataset_id}?isPublic=true&tab=2&version=${dataset_version}`;
break;
}
default:
break;
}
if (url) {
window.open(url, '_blank');
}
});
};
const handleTooltipsMouseEnter = () => {
setEnterTooltip(true);
};
const handleTooltipsMouseLeave = () => {
setEnterTooltip(false);
};
// 获取模型依赖
const getModelAtlas = async () => {
const params = {
model_id: resourceId,
version,
};
const [res] = await to(getModelAtlasReq(params));
if (res && res.data) {
const data = normalizeTreeData(res.data, resourceName);
const graphData = getGraphData(data);
graph.data(graphData);
graph.render();
graph.fitView();
}
};
return (
<div className={styles['model-evolution']}>
<Flex align="center" className={styles['model-evolution__top']}>
<span style={{ marginRight: '10px' }}></span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
allowClear
onChange={onVersionChange}
options={versionList}
/>
<GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand>
</Flex>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
{(showNodeTooltip || enterTooltip) && (
<NodeTooltips
x={nodeTooltipX}
y={nodeTooltipY}
data={hoverNodeData!}
onMouseEnter={handleTooltipsMouseEnter}
onMouseLeave={handleTooltipsMouseLeave}
/>
)}
</div>
);
}
export default ModelEvolution;

View File

@ -0,0 +1,56 @@
.node-tooltips {
position: absolute;
top: -100px;
left: -300px;
width: 300px;
padding: 10px;
background: white;
border: 1px solid #eaeaea;
border-radius: 4px;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
&__title {
margin: 10px 0;
color: @text-color;
font-weight: 500;
font-size: @font-size-content;
}
&__row {
display: flex;
align-items: center;
margin: 4px 0;
color: @text-color;
font-size: 14px;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 10px;
}
&__title {
display: inline-block;
width: 100px;
color: @text-color-secondary;
text-align: right;
}
&__value {
flex: 1;
min-width: 0;
color: @text-color;
font-weight: 500;
.singleLine();
}
&__link {
flex: 1;
min-width: 0;
font-weight: 500;
.singleLine();
}
}
}

View File

@ -0,0 +1,73 @@
import { useNavigate } from '@umijs/max';
import { useEffect } from 'react';
import { ModelDepsData } from '../ModelEvolution';
import styles from './index.less';
type NodeTooltipsProps = {
data: ModelDepsData;
x: number;
y: number;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};
function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) {
const navigate = useNavigate();
useEffect(() => {}, []);
const gotoExperimentPage = () => {
if (data.train_task?.ins_id) {
navigate(`/pipeline/experiment/144/${data.train_task.ins_id}`);
}
};
return (
<div
className={styles['node-tooltips']}
style={{ left: `${x}px`, top: `${y}px` }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className={styles['node-tooltips__title']}></div>
<div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.current_model_name}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
</div>
</div>
<div className={styles['node-tooltips__title']}></div>
<div>
<div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}></span>
{data.train_task?.name ? (
<a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}>
{data.train_task?.name}
</a>
) : null}
</div>
</div>
</div>
);
}
export default NodeTooltips;

View File

@ -1,4 +1,5 @@
.collapse {
flex: none;
width: 250px;
height: 100%;
@ -35,14 +36,15 @@
align-items: center;
height: 40px;
padding: 0 16px;
color: #575757;
color: @text-color-secondary;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.collapseItem:hover {
color: #1664ff;
background: rgba(22, 100, 255, 0.08);
&:hover {
color: @primary-color;
background: rgba(22, 100, 255, 0.08);
}
}
.modelMenusTitle {
margin-bottom: 10px;

View File

@ -75,6 +75,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => {
return (
<div className={Styles.collapse}>
<div className={Styles.modelMenusTitle}></div>
{/* 这样 defaultActiveKey 才能生效 */}
{modelMenusList.length > 0 ? (
<Collapse
collapsible="header"

View File

@ -4,7 +4,8 @@
background-color: #fff;
&__workflow {
flex: 1;
flex: 1 1 0;
min-width: 0;
height: 100%;
&__top {
@ -12,15 +13,15 @@
align-items: center;
justify-content: end;
width: 100%;
height: 45px;
padding: 0 30px;
height: 52px;
padding: 0 20px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
}
&__graph {
width: 100%;
height: calc(100% - 45px);
height: calc(100% - 52px);
background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;

View File

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

View File

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

View File

@ -130,3 +130,11 @@ export function deleteDataset(id) {
method: 'DELETE',
});
}
// 获取模型依赖
export function getModelAtlasReq(data) {
return request(`/api/mmp/modelDependency/queryModelAtlas`, {
method: 'POST',
data
});
}

View File

@ -62,7 +62,12 @@ export type PipelineNodeModelParameter = {
checkedKeys?: string[]; // ResourceSelectorModal checkedKeys
};
// type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType }
// 修改属性类型
export type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType };
// export type PascalCaseType<T> = {
// [K in keyof T as `${Capitalize<string & K>}`]: T[K];
// }
// 序列化后的流水线节点
export type PipelineNodeModelSerialize = Omit<

View File

@ -4,6 +4,8 @@
* @Description:
*/
import G6 from '@antv/g6';
// 生成 8 位随机数
export function s8() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
@ -29,8 +31,22 @@ export function parseJsonText(text?: string | null): any | null {
}
}
// underscore-to-camelCase
// 判断是否为对象
function isPlainObject(value: any) {
if (value === null || typeof value !== 'object') return false;
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
if (proto.constructor && proto.constructor !== Object) return false;
proto = Object.getPrototypeOf(proto);
}
return true;
}
// underscore to camelCase
export function underscoreToCamelCase(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
return $1.toUpperCase().replace('[-_]', '').replace('_', '');
});
let value = obj[key];
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
value = value.map((item) => underscoreToCamelCase(item));
} else if (isPlainObject(value)) {
value = underscoreToCamelCase(value);
}
newObj[newKey] = value;
@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) {
return newObj;
}
// camelCase-to-underscore
// camelCase to underscore
export function camelCaseToUnderscore(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
let value = obj[key];
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
value = value.map((item) => camelCaseToUnderscore(item));
} else if (isPlainObject(value)) {
value = camelCaseToUnderscore(value);
}
newObj[newKey] = value;
@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) {
return newObj;
}
// null undefined
// null to undefined
export function nullToUndefined(obj: Record<string, any>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value === null) {
newObj[key] = undefined;
} else if (typeof value === 'object' && value !== null) {
} else if (Array.isArray(value)) {
newObj[key] = value.map((item) => nullToUndefined(item));
} else if (isPlainObject(value)) {
newObj[key] = nullToUndefined(value);
} else {
newObj[key] = value;
@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) {
}
return newObj;
}
/**
* Changes the property names of an object based on a mapping provided.
*
* @param obj - The object whose property names need to be changed.
* @param mapping - The mapping of old property names to new property names.
* @return The object with the changed property names.
*/
export function changePropertyName(obj: Record<string, any>, mapping: Record<string, string>) {
if (!isPlainObject(obj)) {
return obj;
}
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
const newKey = mapping.hasOwnProperty(key) ? mapping[key] : key;
if (Array.isArray(value)) {
value = value.map((item) => changePropertyName(item, mapping));
} else if (isPlainObject(value)) {
value = changePropertyName(value, mapping);
}
newObj[newKey] = value;
}
}
return newObj;
}
/**
*
* @param tr
* @param maxWidth
* @param fontSize
* @return
*/
export const fittingString = (str: string, maxWidth: number, fontSize: number) => {
if (!str) {
return '';
}
const ellipsis = '...';
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substring(0, i)}${ellipsis}`;
}
});
return res;
};

View File

@ -4,6 +4,7 @@
* @Description: UI
*/
import { PageEnum } from '@/enums/pagesEnums';
import { removeAllPageCacheState } from '@/hooks/pageCacheState';
import themes from '@/styles/theme.less';
import { history } from '@umijs/max';
import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd';
@ -60,6 +61,7 @@ export const gotoLoginPage = (toHome: boolean = true) => {
console.log('search', search);
if (window.location.pathname !== PageEnum.LOGIN) {
closeAllModals();
removeAllPageCacheState();
history.replace({
pathname: PageEnum.LOGIN,
search: newSearch,