Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
a581c37596
|
@ -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",
|
||||
|
|
|
@ -21,6 +21,7 @@ import './styles/menu.less';
|
|||
export { requestConfig as request } from './requestConfig';
|
||||
// const isDev = process.env.NODE_ENV === 'development';
|
||||
import { menuItemRender } from '@/utils/menuRender';
|
||||
import { gotoLoginPage } from './utils/ui';
|
||||
/**
|
||||
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
|
||||
* */
|
||||
|
@ -45,7 +46,7 @@ export async function getInitialState(): Promise<{
|
|||
} as API.CurrentUser;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
history.push(PageEnum.LOGIN);
|
||||
gotoLoginPage();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@ -97,7 +98,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
|
|||
const { location } = history;
|
||||
// 如果没有登录,重定向到 login
|
||||
if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) {
|
||||
history.push(PageEnum.LOGIN);
|
||||
gotoLoginPage();
|
||||
}
|
||||
},
|
||||
layoutBgImgList: [
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 赵伟
|
||||
* @Date: 2024-04-15 10:01:29
|
||||
* @Description:
|
||||
* @Description: 自定义 hooks
|
||||
*/
|
||||
import { FormInstance } from 'antd';
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -126,3 +126,28 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => {
|
|||
}
|
||||
}, [form, prevOpen, open]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the effect function when the specified condition is true.
|
||||
*
|
||||
* @param effect - The effect function to execute.
|
||||
* @param deps - The dependencies for the effect.
|
||||
* @param when - The condition to trigger the effect.
|
||||
*/
|
||||
export const useEffectWhen = (effect: () => void, deps: React.DependencyList, when: boolean) => {
|
||||
const requestFns = useRef<(() => void)[]>([]);
|
||||
useEffect(() => {
|
||||
if (when) {
|
||||
effect();
|
||||
} else {
|
||||
requestFns.current.splice(0, 1, effect);
|
||||
}
|
||||
}, deps);
|
||||
|
||||
useEffect(() => {
|
||||
if (when) {
|
||||
const fn = requestFns.current.pop();
|
||||
fn?.();
|
||||
}
|
||||
}, [when]);
|
||||
};
|
||||
|
|
|
@ -2,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 {
|
||||
|
|
|
@ -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';
|
||||
|
@ -10,16 +11,27 @@ 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 locationParams = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isPublic = searchParams.get('isPublic') === 'true';
|
||||
const defaultTab = searchParams.get('tab') || '1';
|
||||
let versionParam = searchParams.get('version');
|
||||
const [versionList, setVersionList] = useState([]);
|
||||
const [version, setVersion] = useState<string | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState<string>(defaultTab);
|
||||
const resourceId = Number(locationParams.id);
|
||||
const name = resourceConfig[resourceType].name;
|
||||
const typeName = resourceConfig[resourceType].name; // 数据集/模型
|
||||
|
||||
useEffect(() => {
|
||||
getModelByDetail();
|
||||
getVersionList();
|
||||
}, []);
|
||||
|
||||
// 获取详情
|
||||
|
@ -31,10 +43,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}简介`,
|
||||
key: TabKeys.Introduction,
|
||||
label: `${typeName}简介`,
|
||||
children: (
|
||||
<>
|
||||
<div className={styles['resource-intro__title']}>简介</div>
|
||||
|
@ -43,19 +84,41 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
|
|||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: `${name}文件/版本`,
|
||||
key: TabKeys.Version,
|
||||
label: `${typeName}文件/版本`,
|
||||
children: (
|
||||
<ResourceVersion
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
resourceName={info.name}
|
||||
isPublic={isPublic}
|
||||
isPublic={info.available_range === 1}
|
||||
versionList={versionList}
|
||||
version={version}
|
||||
isActive={activeTab === TabKeys.Version}
|
||||
getVersionList={getVersionList}
|
||||
onVersionChange={handleVersionChange}
|
||||
></ResourceVersion>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (resourceType === ResourceType.Model) {
|
||||
items.push({
|
||||
key: TabKeys.Evolution,
|
||||
label: `模型演化`,
|
||||
children: (
|
||||
<ModelEvolution
|
||||
resourceId={resourceId}
|
||||
resourceName={info.name}
|
||||
versionList={versionList}
|
||||
version={version}
|
||||
isActive={activeTab === TabKeys.Evolution}
|
||||
onVersionChange={handleVersionChange}
|
||||
></ModelEvolution>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const infoTypePropertyName = resourceConfig[resourceType]
|
||||
.infoTypePropertyName as keyof ResourceData;
|
||||
const infoTagPropertyName = resourceConfig[resourceType]
|
||||
|
@ -64,21 +127,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}
|
||||
{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 activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -130,7 +130,7 @@ function ResourceList(
|
|||
activeTag: dataTag,
|
||||
});
|
||||
const prefix = resourceConfig[resourceType].prefix;
|
||||
navigate(`/dataset/${prefix}/${record.id}?isPublic=${isPublic}`);
|
||||
navigate(`/dataset/${prefix}/${record.id}`);
|
||||
};
|
||||
|
||||
// 分页切换
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import CommonTableCell from '@/components/CommonTableCell';
|
||||
import DateTableCell from '@/components/DateTableCell';
|
||||
import KFIcon from '@/components/KFIcon';
|
||||
import { useEffectWhen } from '@/hooks';
|
||||
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
|
||||
import { ResourceType } from '@/pages/Dataset/config';
|
||||
import {
|
||||
ResourceFileData,
|
||||
ResourceType,
|
||||
ResourceVersionData,
|
||||
resourceConfig,
|
||||
} from '@/pages/Dataset/config';
|
||||
import { downLoadZip } from '@/utils/downloadfile';
|
||||
import { openAntdModal } from '@/utils/modal';
|
||||
import { to } from '@/utils/promise';
|
||||
import { modalConfirm } from '@/utils/ui';
|
||||
import { App, Button, Flex, Select, Table } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ResourceFileData, resourceConfig } from '../../config';
|
||||
import { useState } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
type ResourceVersionProps = {
|
||||
|
@ -17,42 +22,38 @@ type ResourceVersionProps = {
|
|||
resourceId: number;
|
||||
resourceName: string;
|
||||
isPublic: boolean;
|
||||
versionList: ResourceVersionData[];
|
||||
version?: string;
|
||||
isActive: boolean;
|
||||
getVersionList: () => void;
|
||||
onVersionChange: (version: string) => void;
|
||||
};
|
||||
function ResourceVersion({
|
||||
resourceType,
|
||||
resourceId,
|
||||
resourceName,
|
||||
isPublic,
|
||||
versionList,
|
||||
version,
|
||||
isActive,
|
||||
getVersionList,
|
||||
onVersionChange,
|
||||
}: ResourceVersionProps) {
|
||||
const [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]);
|
||||
// 获取版本文件列表
|
||||
useEffectWhen(
|
||||
() => {
|
||||
if (version) {
|
||||
getFileList(version);
|
||||
} else {
|
||||
setVersion(undefined);
|
||||
setFileList([]);
|
||||
}
|
||||
};
|
||||
},
|
||||
[resourceId, version],
|
||||
isActive,
|
||||
);
|
||||
|
||||
// 获取版本下的文件列表
|
||||
const getFileList = async (version: string) => {
|
||||
|
@ -120,16 +121,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 +185,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" />}>
|
||||
|
|
|
@ -148,12 +148,19 @@ export type ResourceData = {
|
|||
description: string;
|
||||
create_by: string;
|
||||
update_time: string;
|
||||
available_range: number;
|
||||
model_type_name?: string;
|
||||
model_tag_name?: string;
|
||||
dataset_type_name?: string;
|
||||
dataset_tag_name?: string;
|
||||
};
|
||||
|
||||
// 版本数据
|
||||
export type ResourceVersionData = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
// 版本文件数据
|
||||
export type ResourceFileData = {
|
||||
id: number;
|
||||
|
|
|
@ -22,4 +22,3 @@ function DatasetAnnotation() {
|
|||
}
|
||||
|
||||
export default DatasetAnnotation;
|
||||
|
||||
|
|
|
@ -99,7 +99,10 @@ function LogGroup({
|
|||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// 判断是否日志是否加载完成
|
||||
if (!log_detail?.log_content) {
|
||||
setCompleted(true);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { useStateRef, useVisible } from '@/hooks';
|
||||
import { getExperimentIns } from '@/services/experiment/index.js';
|
||||
import { getWorkflowById } from '@/services/pipeline/index.js';
|
||||
import themes from '@/styles/theme.less';
|
||||
import { fittingString } from '@/utils';
|
||||
import { elapsedTime, formatDate } from '@/utils/date';
|
||||
import G6 from '@antv/g6';
|
||||
import { Button } from 'antd';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { s8 } from '../../../utils';
|
||||
import ParamsModal from '../components/ViewParamsModal';
|
||||
import { experimentStatusInfo } from '../status';
|
||||
import styles from './index.less';
|
||||
|
@ -22,27 +23,22 @@ function ExperimentText() {
|
|||
const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);
|
||||
|
||||
const graphRef = useRef();
|
||||
const onDragEnd = (val) => {
|
||||
console.log(val, 'eee');
|
||||
const _x = val.x;
|
||||
const _y = val.y;
|
||||
const point = graph.getPointByClient(_x, _y);
|
||||
let model = {};
|
||||
// 元模型
|
||||
model = {
|
||||
...val,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
id: val.component_name + '-' + s8(),
|
||||
isCluster: false,
|
||||
};
|
||||
graph.addItem('node', model, true);
|
||||
};
|
||||
const handlerClick = (e) => {
|
||||
if (e.target.get('name') !== 'anchor-point' && e.item) {
|
||||
propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
|
||||
}
|
||||
};
|
||||
// const onDragEnd = (val) => {
|
||||
// console.log(val, 'eee');
|
||||
// const _x = val.x;
|
||||
// const _y = val.y;
|
||||
// const point = graph.getPointByClient(_x, _y);
|
||||
// let model = {};
|
||||
// // 元模型
|
||||
// model = {
|
||||
// ...val,
|
||||
// x: point.x,
|
||||
// y: point.y,
|
||||
// id: val.component_name + '-' + s8(),
|
||||
// isCluster: false,
|
||||
// };
|
||||
// graph.addItem('node', model, true);
|
||||
// };
|
||||
const getGraphData = (data) => {
|
||||
if (graph) {
|
||||
graph.data(data);
|
||||
|
@ -89,32 +85,6 @@ function ExperimentText() {
|
|||
}, []);
|
||||
|
||||
const initGraph = () => {
|
||||
const fittingString = (str, maxWidth, fontSize) => {
|
||||
const ellipsis = '...';
|
||||
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
|
||||
let currentWidth = 0;
|
||||
let res = str;
|
||||
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
|
||||
str.split('').forEach((letter, i) => {
|
||||
if (currentWidth > maxWidth - ellipsisLength) return;
|
||||
if (pattern.test(letter)) {
|
||||
// Chinese charactors
|
||||
currentWidth += fontSize;
|
||||
} else {
|
||||
// get the width of single letter according to the fontSize
|
||||
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
|
||||
}
|
||||
if (currentWidth > maxWidth - ellipsisLength) {
|
||||
res = `${str.substr(0, i)}${ellipsis}`;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
// 获取文本的长度
|
||||
const getTextSize = (str, maxWidth, fontSize) => {
|
||||
let width = G6.Util.getTextSize(str, fontSize)[0];
|
||||
return width > maxWidth ? maxWidth : width;
|
||||
};
|
||||
G6.registerNode(
|
||||
'rect-node',
|
||||
{
|
||||
|
@ -129,7 +99,6 @@ function ExperimentText() {
|
|||
);
|
||||
},
|
||||
afterDraw(cfg, group) {
|
||||
// console.log(group, cfg, 12312);
|
||||
const image = group.addShape('image', {
|
||||
attrs: {
|
||||
x: -45,
|
||||
|
@ -158,7 +127,6 @@ function ExperimentText() {
|
|||
}
|
||||
const bbox = group.getBBox();
|
||||
const anchorPoints = this.getAnchorPoints(cfg);
|
||||
// console.log(anchorPoints);
|
||||
anchorPoints.forEach((anchorPos, i) => {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
|
@ -179,19 +147,19 @@ function ExperimentText() {
|
|||
|
||||
// response the state changes and show/hide the link-point circles
|
||||
setState(name, value, item) {
|
||||
const anchorPoints = item
|
||||
.getContainer()
|
||||
.findAll((ele) => ele.get('name') === 'anchor-point');
|
||||
anchorPoints.forEach((point) => {
|
||||
if (value || point.get('links') > 0) point.show();
|
||||
else point.hide();
|
||||
});
|
||||
// }
|
||||
const group = item.getContainer();
|
||||
const shape = group.get('children')[0];
|
||||
if (name === 'hover') {
|
||||
if (value) {
|
||||
shape.attr('stroke', themes['primaryColor']);
|
||||
} else {
|
||||
shape.attr('stroke', '#fff');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'rect',
|
||||
);
|
||||
console.log(graphRef, 'graphRef');
|
||||
graph = new G6.Graph({
|
||||
container: graphRef.current,
|
||||
grid: true,
|
||||
|
@ -209,10 +177,6 @@ function ExperimentText() {
|
|||
if (e.target.get('name') === 'anchor-point') return false;
|
||||
return true;
|
||||
},
|
||||
// shouldEnd: e => {
|
||||
// console.log(e);
|
||||
// return false;
|
||||
// },
|
||||
},
|
||||
// config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles
|
||||
'drag-canvas',
|
||||
|
@ -237,7 +201,6 @@ function ExperimentText() {
|
|||
style: {
|
||||
fill: '#000',
|
||||
fontSize: 10,
|
||||
|
||||
cursor: 'pointer',
|
||||
x: -20,
|
||||
y: 0,
|
||||
|
@ -252,17 +215,6 @@ function ExperimentText() {
|
|||
lineWidth: 0.5,
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
nodeSelected: {
|
||||
fill: 'red',
|
||||
shadowColor: 'red',
|
||||
stroke: 'red',
|
||||
'text-shape': {
|
||||
fill: 'red',
|
||||
stroke: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
// type: 'quadratic',
|
||||
type: 'cubic-vertical',
|
||||
|
@ -308,15 +260,25 @@ function ExperimentText() {
|
|||
// linkCenter: true,
|
||||
fitView: true,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 3,
|
||||
fitViewPadding: [320, 320, 220, 320],
|
||||
maxZoom: 5,
|
||||
fitViewPadding: 300,
|
||||
});
|
||||
graph.on('node:click', (e) => {
|
||||
if (e.target.get('name') !== 'anchor-point' && e.item) {
|
||||
propsRef.current.showDrawer(e, locationParams.id, messageRef.current);
|
||||
}
|
||||
});
|
||||
graph.on('node:mouseenter', (e) => {
|
||||
graph.setItemState(e.item, 'hover', true);
|
||||
});
|
||||
graph.on('node:mouseleave', (e) => {
|
||||
graph.setItemState(e.item, 'hover', false);
|
||||
});
|
||||
graph.on('node:click', handlerClick);
|
||||
window.onresize = () => {
|
||||
if (!graph || graph.get('destroyed')) return;
|
||||
if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight)
|
||||
return;
|
||||
graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20);
|
||||
if (!graphRef.current) return;
|
||||
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
|
||||
graph.fitView();
|
||||
};
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -20,7 +20,7 @@ import { formatDate } from '@/utils/date';
|
|||
import { to } from '@/utils/promise';
|
||||
import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage';
|
||||
import { modalConfirm } from '@/utils/ui';
|
||||
import { useNavigate, useParams, useSearchParams } from '@umijs/max';
|
||||
import { useNavigate, useParams } from '@umijs/max';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
|
@ -33,7 +33,7 @@ import {
|
|||
type TableProps,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import MirrorStatusCell from '../components/MirrorStatusCell';
|
||||
import styles from './index.less';
|
||||
|
||||
|
@ -42,6 +42,7 @@ type MirrorInfoData = {
|
|||
description?: string;
|
||||
version_count?: string;
|
||||
create_time?: string;
|
||||
image_type?: number;
|
||||
};
|
||||
|
||||
type MirrorVersionData = {
|
||||
|
@ -56,7 +57,6 @@ type MirrorVersionData = {
|
|||
function MirrorInfo() {
|
||||
const navigate = useNavigate();
|
||||
const urlParams = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [cacheState, setCacheState] = useCacheState();
|
||||
const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({});
|
||||
const [tableData, setTableData] = useState<MirrorVersionData[]>([]);
|
||||
|
@ -69,7 +69,7 @@ function MirrorInfo() {
|
|||
},
|
||||
);
|
||||
const { message } = App.useApp();
|
||||
const isPublic = searchParams.get('isPublic') === 'true';
|
||||
const isPublic = useMemo(() => mirrorInfo.image_type === 1, [mirrorInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
getMirrorInfo();
|
||||
|
@ -84,14 +84,7 @@ function MirrorInfo() {
|
|||
const id = Number(urlParams.id);
|
||||
const [res] = await to(getMirrorInfoReq(id));
|
||||
if (res && res.data) {
|
||||
const { name = '', description = '', version_count = '', create_time: time } = res.data;
|
||||
const create_time = formatDate(time);
|
||||
setMirrorInfo({
|
||||
name,
|
||||
description,
|
||||
version_count,
|
||||
create_time,
|
||||
});
|
||||
setMirrorInfo(res.data);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -258,7 +251,7 @@ function MirrorInfo() {
|
|||
<Col span={10}>
|
||||
<div className={styles['mirror-info__basic__item']}>
|
||||
<div className={styles['label']}>创建时间:</div>
|
||||
<div className={styles['value']}>{mirrorInfo.create_time}</div>
|
||||
<div className={styles['value']}>{formatDate(mirrorInfo.create_time)}</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -270,7 +263,7 @@ function MirrorInfo() {
|
|||
></SubAreaTitle>
|
||||
{!isPublic && (
|
||||
<Button
|
||||
style={{ marginRight: 0, marginLeft: 'auto' }}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
type="default"
|
||||
onClick={createMirrorVersion}
|
||||
icon={<KFIcon type="icon-xinjian2" />}
|
||||
|
@ -279,7 +272,7 @@ function MirrorInfo() {
|
|||
</Button>
|
||||
)}
|
||||
<Button
|
||||
style={{ marginLeft: '20px' }}
|
||||
style={{ marginLeft: isPublic ? 'auto' : '20px', marginRight: 0 }}
|
||||
type="default"
|
||||
onClick={getMirrorVersionList}
|
||||
icon={<KFIcon type="icon-shuaxin" />}
|
||||
|
|
|
@ -125,7 +125,7 @@ function MirrorList() {
|
|||
|
||||
// 查看详情
|
||||
const toDetail = (record: MirrorData) => {
|
||||
navigate(`/dataset/mirror/${record.id}?isPublic=${activeTab === CommonTabKeys.Public}`);
|
||||
navigate(`/dataset/mirror/${record.id}`);
|
||||
setCacheState({
|
||||
activeTab,
|
||||
pagination,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,521 @@
|
|||
import { useEffectWhen } from '@/hooks';
|
||||
import { ResourceVersionData } from '@/pages/Dataset/config';
|
||||
import { getModelAtlasReq } from '@/services/dataset/index.js';
|
||||
import themes from '@/styles/theme.less';
|
||||
import { changePropertyName, fittingString } from '@/utils';
|
||||
import { to } from '@/utils/promise';
|
||||
import G6, {
|
||||
EdgeConfig,
|
||||
G6GraphEvent,
|
||||
Graph,
|
||||
GraphData,
|
||||
LayoutConfig,
|
||||
NodeConfig,
|
||||
TreeGraphData,
|
||||
Util,
|
||||
} from '@antv/g6';
|
||||
// @ts-ignore
|
||||
import Hierarchy from '@antv/hierarchy';
|
||||
import { Flex, Select } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import GraphLegand from '../GraphLegand';
|
||||
import NodeTooltips from '../NodeTooltips';
|
||||
import styles from './index.less';
|
||||
|
||||
const nodeWidth = 98;
|
||||
const nodeHeight = 58;
|
||||
const vGap = 58;
|
||||
const hGap = 58;
|
||||
|
||||
enum NodeType {
|
||||
current = 'current',
|
||||
parent = 'parent',
|
||||
children = 'children',
|
||||
project = 'project',
|
||||
trainDataset = 'trainDataset',
|
||||
testDataset = 'testDataset',
|
||||
}
|
||||
|
||||
type TrainTask = {
|
||||
ins_id: number;
|
||||
name: string;
|
||||
task_id: string;
|
||||
};
|
||||
|
||||
interface TrainDataset extends NodeConfig {
|
||||
dataset_id: number;
|
||||
dataset_name: string;
|
||||
dataset_version: string;
|
||||
model_type: NodeType;
|
||||
}
|
||||
|
||||
interface ProjectDependency extends NodeConfig {
|
||||
url: string;
|
||||
name: string;
|
||||
branch: string;
|
||||
model_type: NodeType;
|
||||
}
|
||||
|
||||
type ModalDetail = {
|
||||
name: string;
|
||||
available_range: number;
|
||||
file_name: string;
|
||||
file_size: string;
|
||||
description: string;
|
||||
model_type_name: string;
|
||||
model_tag_name: string;
|
||||
create_time: string;
|
||||
};
|
||||
|
||||
interface ModelDepsAPIData {
|
||||
current_model_id: number;
|
||||
version: string;
|
||||
exp_ins_id: number;
|
||||
model_type: NodeType;
|
||||
current_model_name: string;
|
||||
project_dependency: ProjectDependency;
|
||||
test_dataset: TrainDataset[];
|
||||
train_dataset: TrainDataset[];
|
||||
train_task: TrainTask;
|
||||
model_version_dependcy_vo: ModalDetail;
|
||||
children_models: ModelDepsAPIData[];
|
||||
parent_models: ModelDepsAPIData[];
|
||||
}
|
||||
|
||||
export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData {
|
||||
children: ModelDepsData[];
|
||||
}
|
||||
|
||||
// 规范化子数据
|
||||
function normalizeChildren(data: ModelDepsData[]) {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item) => {
|
||||
item.model_type = NodeType.children;
|
||||
item.id = `$M_${item.current_model_id}_${item.version}`;
|
||||
item.label = getLabel(item);
|
||||
item.style = getStyle(NodeType.children);
|
||||
normalizeChildren(item.children);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 label
|
||||
function getLabel(node: { current_model_name: string; version: string }) {
|
||||
return (
|
||||
fittingString(`${node.current_model_name}`, 87, 8) +
|
||||
'\n' +
|
||||
fittingString(`${node.version}`, 87, 8)
|
||||
);
|
||||
}
|
||||
|
||||
// 获取 style
|
||||
function getStyle(model_type: NodeType) {
|
||||
let fill = '';
|
||||
switch (model_type) {
|
||||
case NodeType.current:
|
||||
fill = '#1664ff';
|
||||
break;
|
||||
case NodeType.parent:
|
||||
fill = '#76b1ff';
|
||||
break;
|
||||
case NodeType.children:
|
||||
fill = '#b7cfff';
|
||||
break;
|
||||
case NodeType.project:
|
||||
fill = '#FA8C16';
|
||||
break;
|
||||
case NodeType.trainDataset:
|
||||
fill = '#ff0000';
|
||||
break;
|
||||
case NodeType.testDataset:
|
||||
fill = '#ff00ff';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
fill,
|
||||
};
|
||||
}
|
||||
|
||||
// 将后台返回的数据转换成树形数据
|
||||
function normalizeTreeData(apiData: ModelDepsAPIData, currentNodeName: string): ModelDepsData {
|
||||
// 将 children_models 转换成 children
|
||||
let normalizedData = changePropertyName(apiData, {
|
||||
children_models: 'children',
|
||||
}) as ModelDepsData;
|
||||
|
||||
// 设置当前模型的数据
|
||||
normalizedData.model_type = NodeType.current;
|
||||
normalizedData.current_model_name = currentNodeName;
|
||||
normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
|
||||
normalizedData.label = getLabel(normalizedData);
|
||||
normalizedData.style = getStyle(NodeType.current);
|
||||
|
||||
normalizeChildren(normalizedData.children as ModelDepsData[]);
|
||||
|
||||
// 将 parent_models 转换成树形结构
|
||||
let parent_models = normalizedData.parent_models || [];
|
||||
while (parent_models.length > 0) {
|
||||
const parent = parent_models[0];
|
||||
normalizedData = {
|
||||
...parent,
|
||||
model_type: NodeType.parent,
|
||||
id: `$M_${parent.current_model_id}_${parent.version}`,
|
||||
label: getLabel(parent),
|
||||
style: getStyle(NodeType.parent),
|
||||
children: [
|
||||
{
|
||||
...normalizedData,
|
||||
parent_models: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
parent_models = normalizedData.parent_models || [];
|
||||
}
|
||||
return normalizedData;
|
||||
}
|
||||
|
||||
// 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据
|
||||
function getGraphData(data: ModelDepsData): GraphData {
|
||||
const config = {
|
||||
direction: 'LR',
|
||||
getHeight: () => nodeHeight,
|
||||
getWidth: () => nodeWidth,
|
||||
getVGap: () => vGap / 2,
|
||||
getHGap: () => hGap / 2,
|
||||
};
|
||||
|
||||
// 树形布局计算出坐标
|
||||
const treeLayoutData: LayoutConfig = Hierarchy['compactBox'](data, config);
|
||||
|
||||
const nodes: NodeConfig[] = [];
|
||||
const edges: EdgeConfig[] = [];
|
||||
Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => {
|
||||
const data = node.data as ModelDepsData;
|
||||
nodes.push({
|
||||
...data,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
});
|
||||
if (parent) {
|
||||
edges.push({
|
||||
source: parent.id,
|
||||
target: node.id,
|
||||
});
|
||||
}
|
||||
|
||||
// 当前模型显示数据集和项目
|
||||
if (data.model_type === NodeType.current) {
|
||||
const { project_dependency, train_dataset, test_dataset } = data;
|
||||
train_dataset.forEach((item) => {
|
||||
item.id = `$DTrain_${item.dataset_id}`;
|
||||
item.model_type = NodeType.trainDataset;
|
||||
item.type = 'ellipse';
|
||||
item.label = fittingString(`${item.dataset_name}`, 87, 8);
|
||||
item.style = getStyle(NodeType.trainDataset);
|
||||
});
|
||||
test_dataset.forEach((item) => {
|
||||
item.id = `$DTest_${item.dataset_id}`;
|
||||
item.model_type = NodeType.testDataset;
|
||||
item.type = 'ellipse';
|
||||
item.label = fittingString(item.dataset_name, 87, 8);
|
||||
item.style = getStyle(NodeType.testDataset);
|
||||
});
|
||||
|
||||
const len = train_dataset.length + test_dataset.length;
|
||||
[...train_dataset, ...test_dataset].forEach((item, index) => {
|
||||
const half = len / 2 - 0.5;
|
||||
item.x = node.x! - (half - index) * (nodeWidth + hGap);
|
||||
item.y = node.y! - nodeHeight - vGap;
|
||||
nodes.push(item);
|
||||
edges.push({
|
||||
source: node.id,
|
||||
target: item.id,
|
||||
sourceAnchor: 2,
|
||||
targetAnchor: 3,
|
||||
type: 'cubic-vertical',
|
||||
});
|
||||
});
|
||||
|
||||
if (project_dependency?.url) {
|
||||
project_dependency.id = `$P_${project_dependency.url}`;
|
||||
project_dependency.model_type = NodeType.project;
|
||||
project_dependency.type = 'rect';
|
||||
project_dependency.size = [nodeHeight, nodeHeight];
|
||||
project_dependency.label = fittingString(project_dependency.name, 48, 8);
|
||||
project_dependency.style = getStyle(NodeType.project);
|
||||
project_dependency.x = node.x;
|
||||
project_dependency.y = node.y! + nodeHeight + vGap;
|
||||
nodes.push(project_dependency);
|
||||
edges.push({
|
||||
source: node.id,
|
||||
target: project_dependency.id,
|
||||
sourceAnchor: 3,
|
||||
targetAnchor: 2,
|
||||
type: 'cubic-vertical',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
type modeModelEvolutionProps = {
|
||||
resourceId: number;
|
||||
resourceName: string;
|
||||
versionList: ResourceVersionData[];
|
||||
version?: string;
|
||||
isActive: boolean;
|
||||
onVersionChange: (version: string) => void;
|
||||
};
|
||||
|
||||
let graph: Graph;
|
||||
function ModelEvolution({
|
||||
resourceId,
|
||||
resourceName,
|
||||
versionList,
|
||||
version,
|
||||
isActive,
|
||||
onVersionChange,
|
||||
}: modeModelEvolutionProps) {
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [showNodeTooltip, setShowNodeTooltip] = useState(false);
|
||||
const [enterTooltip, setEnterTooltip] = useState(false);
|
||||
const [nodeTooltipX, setNodeToolTipX] = useState(0);
|
||||
const [nodeTooltipY, setNodeToolTipY] = useState(0);
|
||||
const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
initGraph();
|
||||
const changSize = () => {
|
||||
if (!graph || graph.get('destroyed')) return;
|
||||
if (!graphRef.current) return;
|
||||
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
|
||||
graph.fitView();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', changSize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', changSize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffectWhen(
|
||||
() => {
|
||||
if (version) {
|
||||
getModelAtlas();
|
||||
} else {
|
||||
clearGraphData();
|
||||
}
|
||||
},
|
||||
[resourceId, version],
|
||||
isActive,
|
||||
);
|
||||
|
||||
// 初始化图
|
||||
const initGraph = () => {
|
||||
graph = new G6.Graph({
|
||||
container: graphRef.current!,
|
||||
width: graphRef.current!.clientWidth,
|
||||
height: graphRef.current!.clientHeight,
|
||||
fitView: true,
|
||||
fitViewPadding: [50, 100, 50, 100],
|
||||
minZoom: 0.5,
|
||||
maxZoom: 5,
|
||||
defaultNode: {
|
||||
type: 'rect',
|
||||
size: [nodeWidth, nodeHeight],
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
[0.5, 0],
|
||||
[0.5, 1],
|
||||
],
|
||||
style: {
|
||||
fill: themes['primaryColor'],
|
||||
lineWidth: 0,
|
||||
radius: 6,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
labelCfg: {
|
||||
position: 'center',
|
||||
style: {
|
||||
fill: '#ffffff',
|
||||
fontSize: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
type: 'cubic-horizontal',
|
||||
labelCfg: {
|
||||
autoRotate: true,
|
||||
},
|
||||
style: {
|
||||
stroke: '#a2c1ff',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
modes: {
|
||||
default: [
|
||||
'drag-canvas',
|
||||
'zoom-canvas',
|
||||
// {
|
||||
// type: 'collapse-expand',
|
||||
// onChange(item?: Item, collapsed?: boolean) {
|
||||
// const data = item!.getModel();
|
||||
// data.collapsed = collapsed;
|
||||
// return true;
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
bindEvents();
|
||||
};
|
||||
|
||||
// 绑定事件
|
||||
const bindEvents = () => {
|
||||
graph.on('node:mouseenter', (e: G6GraphEvent) => {
|
||||
const nodeItem = e.item;
|
||||
graph.setItemState(nodeItem, 'hover', true);
|
||||
|
||||
const model = nodeItem.getModel() as ModelDepsData;
|
||||
const { x, y, model_type } = model;
|
||||
if (
|
||||
model_type === NodeType.project ||
|
||||
model_type === NodeType.trainDataset ||
|
||||
model_type === NodeType.testDataset
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const point = graph.getCanvasByPoint(x!, y!);
|
||||
const canvasWidth = graphRef.current!.clientWidth;
|
||||
if (point.x + 300 > canvasWidth) {
|
||||
point.x = canvasWidth - 300;
|
||||
}
|
||||
const zoom = graph.getZoom();
|
||||
// 更加缩放,调整 tooltip 位置
|
||||
const offsetY = (nodeHeight * zoom) / 4;
|
||||
|
||||
setHoverNodeData(model);
|
||||
setNodeToolTipX(point.x);
|
||||
// 92: 版本选择器的高度,296: tooltip的高度
|
||||
setNodeToolTipY(point.y + 92 - 296 - offsetY);
|
||||
setShowNodeTooltip(true);
|
||||
});
|
||||
|
||||
graph.on('node:mouseleave', (e: G6GraphEvent) => {
|
||||
const nodeItem = e.item;
|
||||
graph.setItemState(nodeItem, 'hover', false);
|
||||
setShowNodeTooltip(false);
|
||||
});
|
||||
|
||||
graph.on('node:click', (e: G6GraphEvent) => {
|
||||
const nodeItem = e.item;
|
||||
const model = nodeItem.getModel();
|
||||
const { model_type } = model;
|
||||
const { origin } = location;
|
||||
let url: string = '';
|
||||
switch (model_type) {
|
||||
case NodeType.children:
|
||||
case NodeType.parent: {
|
||||
const { current_model_id, version } = model as ModelDepsData;
|
||||
url = `${origin}/dataset/model/${current_model_id}?tab=3&version=${version}`;
|
||||
break;
|
||||
}
|
||||
case NodeType.project: {
|
||||
const { url: projectUrl } = model as ProjectDependency;
|
||||
url = projectUrl;
|
||||
break;
|
||||
}
|
||||
case NodeType.trainDataset:
|
||||
case NodeType.testDataset: {
|
||||
const { dataset_id, dataset_version } = model as TrainDataset;
|
||||
url = `${origin}/dataset/dataset/${dataset_id}?tab=2&version=${dataset_version}`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
});
|
||||
|
||||
// 鼠标滚轮缩放时,隐藏 tooltip
|
||||
graph.on('wheelzoom', () => {
|
||||
setShowNodeTooltip(false);
|
||||
setEnterTooltip(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTooltipsMouseEnter = () => {
|
||||
setEnterTooltip(true);
|
||||
};
|
||||
|
||||
const handleTooltipsMouseLeave = () => {
|
||||
setEnterTooltip(false);
|
||||
};
|
||||
|
||||
// 获取模型依赖
|
||||
const getModelAtlas = async () => {
|
||||
const params = {
|
||||
model_id: resourceId,
|
||||
version,
|
||||
};
|
||||
const [res] = await to(getModelAtlasReq(params));
|
||||
if (res && res.data) {
|
||||
const data = normalizeTreeData(res.data, resourceName);
|
||||
const graphData = getGraphData(data);
|
||||
|
||||
graph.data(graphData);
|
||||
graph.render();
|
||||
graph.fitView();
|
||||
} else {
|
||||
clearGraphData();
|
||||
}
|
||||
};
|
||||
|
||||
// 请求失败或者版本不存在时,清除图形
|
||||
function clearGraphData() {
|
||||
graph.data({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
graph.render();
|
||||
graph.fitView();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['model-evolution']}>
|
||||
<Flex align="center" className={styles['model-evolution__top']}>
|
||||
<span style={{ marginRight: '10px' }}>版本号:</span>
|
||||
<Select
|
||||
placeholder="请选择版本号"
|
||||
style={{ width: '160px', marginRight: '20px' }}
|
||||
value={version}
|
||||
allowClear
|
||||
onChange={onVersionChange}
|
||||
options={versionList}
|
||||
/>
|
||||
<GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand>
|
||||
</Flex>
|
||||
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
|
||||
{(showNodeTooltip || enterTooltip) && (
|
||||
<NodeTooltips
|
||||
x={nodeTooltipX}
|
||||
y={nodeTooltipY}
|
||||
data={hoverNodeData!}
|
||||
onMouseEnter={handleTooltipsMouseEnter}
|
||||
onMouseLeave={handleTooltipsMouseLeave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModelEvolution;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { formatDate } from '@/utils/date';
|
||||
import { ModelDepsData } from '../ModelEvolution';
|
||||
import styles from './index.less';
|
||||
|
||||
type NodeTooltipsProps = {
|
||||
data: ModelDepsData;
|
||||
x: number;
|
||||
y: number;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
};
|
||||
|
||||
function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) {
|
||||
const gotoExperimentPage = () => {
|
||||
if (data.train_task?.ins_id) {
|
||||
const { origin } = location;
|
||||
window.open(`${origin}/pipeline/experiment/144/${data.train_task.ins_id}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['node-tooltips']}
|
||||
style={{ left: `${x}px`, top: `${y}px` }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className={styles['node-tooltips__title']}>模型信息</div>
|
||||
<div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>模型名称:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>{data.current_model_name}</span>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>模型版本:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>{data.version}</span>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>模型框架:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>
|
||||
{data.model_version_dependcy_vo?.model_type_name || '--'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>模型大小:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>
|
||||
{data.model_version_dependcy_vo?.file_size || '--'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>创建时间:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>
|
||||
{formatDate(data.model_version_dependcy_vo?.create_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>模型权限:</span>
|
||||
<span className={styles['node-tooltips__row__value']}>
|
||||
{data.model_version_dependcy_vo?.available_range === 1 ? '公开' : '私有'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['node-tooltips__title']}>训练相关信息</div>
|
||||
<div>
|
||||
<div className={styles['node-tooltips__row']}>
|
||||
<span className={styles['node-tooltips__row__title']}>训练任务:</span>
|
||||
{data.train_task?.name ? (
|
||||
<a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}>
|
||||
{data.train_task?.name}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeTooltips;
|
|
@ -104,7 +104,7 @@ function ModelDeploymentCreate() {
|
|||
onOk: (res) => {
|
||||
if (res) {
|
||||
if (type === ResourceSelectorType.Mirror) {
|
||||
form.setFieldValue(name, res);
|
||||
form.setFieldValue(name, res.path);
|
||||
} else {
|
||||
const response = res as ResourceSelectorResponse;
|
||||
const showValue = `${response.name}:${response.version}`;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.collapse {
|
||||
flex: none;
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
|
||||
|
@ -35,15 +36,16 @@
|
|||
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;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
background: rgba(22, 100, 255, 0.08);
|
||||
}
|
||||
}
|
||||
.modelMenusTitle {
|
||||
margin-bottom: 10px;
|
||||
padding: 12px 25px;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import KFIcon from '@/components/KFIcon';
|
||||
import { useStateRef, useVisible } from '@/hooks';
|
||||
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
|
||||
import themes from '@/styles/theme.less';
|
||||
import { fittingString } from '@/utils';
|
||||
import { to } from '@/utils/promise';
|
||||
import G6 from '@antv/g6';
|
||||
import { App, Button } from 'antd';
|
||||
|
@ -27,6 +29,11 @@ const EditPipeline = () => {
|
|||
const { message } = App.useApp();
|
||||
let sourceAnchorIdx, targetAnchorIdx;
|
||||
|
||||
useEffect(() => {
|
||||
initMenu();
|
||||
getFirstWorkflow(locationParams.id);
|
||||
}, []);
|
||||
|
||||
const onDragEnd = (val) => {
|
||||
console.log(val);
|
||||
const _x = val.x;
|
||||
|
@ -103,20 +110,8 @@ const EditPipeline = () => {
|
|||
});
|
||||
}, 500);
|
||||
};
|
||||
const handlerClick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.target.get('name') !== 'anchor-point' && e.item) {
|
||||
graph.setItemState(e.item, 'nodeClicked', true);
|
||||
const parentNodes = findAllParentNodes(graph, e.item);
|
||||
// 如果没有打开过全局参数抽屉,获取不到全局参数
|
||||
const globalParams =
|
||||
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
|
||||
propsRef.current.showDrawer(e, globalParams, parentNodes);
|
||||
}
|
||||
};
|
||||
const getGraphData = (data) => {
|
||||
if (graph) {
|
||||
console.log(data);
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
} else {
|
||||
|
@ -312,49 +307,8 @@ const EditPipeline = () => {
|
|||
|
||||
initGraph();
|
||||
};
|
||||
useEffect(() => {
|
||||
initMenu();
|
||||
getFirstWorkflow(locationParams.id);
|
||||
|
||||
return () => {
|
||||
graph.off('node:mouseenter', (e) => {
|
||||
graph.setItemState(e.item, 'showAnchors', true);
|
||||
graph.setItemState(e.item, 'nodeSelected', true);
|
||||
});
|
||||
graph.off('node:mouseleave', (e) => {
|
||||
// this.graph.setItemState(e.item, 'showAnchors', false);
|
||||
graph.setItemState(e.item, 'nodeSelected', false);
|
||||
});
|
||||
// graph.off('dblclick', handlerClick);
|
||||
};
|
||||
}, []);
|
||||
const initGraph = () => {
|
||||
const fittingString = (str, maxWidth, fontSize) => {
|
||||
const ellipsis = '...';
|
||||
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
|
||||
let currentWidth = 0;
|
||||
let res = str;
|
||||
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
|
||||
str.split('').forEach((letter, i) => {
|
||||
if (currentWidth > maxWidth - ellipsisLength) return;
|
||||
if (pattern.test(letter)) {
|
||||
// Chinese charactors
|
||||
currentWidth += fontSize;
|
||||
} else {
|
||||
// get the width of single letter according to the fontSize
|
||||
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
|
||||
}
|
||||
if (currentWidth > maxWidth - ellipsisLength) {
|
||||
res = `${str.substr(0, i)}${ellipsis}`;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
// 获取文本的长度
|
||||
const getTextSize = (str, maxWidth, fontSize) => {
|
||||
let width = G6.Util.getTextSize(str, fontSize)[0];
|
||||
return width > maxWidth ? maxWidth : width;
|
||||
};
|
||||
G6.registerNode(
|
||||
'rect-node',
|
||||
{
|
||||
|
@ -407,6 +361,7 @@ const EditPipeline = () => {
|
|||
y: bbox.y + bbox.height * anchorPos[1],
|
||||
fill: '#fff',
|
||||
stroke: '#a4a4a5',
|
||||
cursor: 'crosshair',
|
||||
},
|
||||
name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
|
||||
anchorPointIdx: i, // flag the idx of the anchor-point circle
|
||||
|
@ -420,14 +375,30 @@ const EditPipeline = () => {
|
|||
|
||||
// response the state changes and show/hide the link-point circles
|
||||
setState(name, value, item) {
|
||||
const anchorPoints = item
|
||||
.getContainer()
|
||||
.findAll((ele) => ele.get('name') === 'anchor-point');
|
||||
// const anchorPoints = item
|
||||
// .getContainer()
|
||||
// .findAll((ele) => ele.get('name') === 'anchor-point');
|
||||
// anchorPoints.forEach((point) => {
|
||||
// if (value || point.get('links') > 0) point.show();
|
||||
// else point.hide();
|
||||
// });
|
||||
|
||||
const group = item.getContainer();
|
||||
const shape = group.get('children')[0];
|
||||
const anchorPoints = group.findAll((ele) => ele.get('name') === 'anchor-point');
|
||||
if (name === 'hover') {
|
||||
if (value) {
|
||||
shape.attr('stroke', themes['primaryColor']);
|
||||
anchorPoints.forEach((point) => {
|
||||
if (value || point.get('links') > 0) point.show();
|
||||
else point.hide();
|
||||
point.show();
|
||||
});
|
||||
// }
|
||||
} else {
|
||||
shape.attr('stroke', '#fff');
|
||||
anchorPoints.forEach((point) => {
|
||||
point.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'rect',
|
||||
|
@ -435,7 +406,6 @@ const EditPipeline = () => {
|
|||
|
||||
graph = new G6.Graph({
|
||||
container: graphRef.current,
|
||||
grid: true,
|
||||
width: graphRef.current.clientWidth || 500,
|
||||
height: graphRef.current.clientHeight || '100%',
|
||||
animate: false,
|
||||
|
@ -519,19 +489,7 @@ const EditPipeline = () => {
|
|||
lineWidth: 0.5,
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
nodeSelected: {
|
||||
fill: 'red',
|
||||
shadowColor: 'red',
|
||||
stroke: 'red',
|
||||
'text-shape': {
|
||||
fill: 'red',
|
||||
stroke: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
// type: 'quadratic',
|
||||
//type: 'cubic-vertical',
|
||||
|
||||
style: {
|
||||
|
@ -575,17 +533,20 @@ const EditPipeline = () => {
|
|||
// linkCenter: true,
|
||||
fitView: true,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 3,
|
||||
fitViewPadding: [320, 320, 220, 320],
|
||||
maxZoom: 5,
|
||||
fitViewPadding: 300,
|
||||
});
|
||||
// graph.on('dblclick', (e) => {
|
||||
// console.log(e.item);
|
||||
// if (e.item) {
|
||||
graph.on('node:click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.target.get('name') !== 'anchor-point' && e.item) {
|
||||
// graph.setItemState(e.item, 'nodeClicked', true);
|
||||
// handlerClick(e);
|
||||
// }
|
||||
// });
|
||||
graph.on('node:click', handlerClick);
|
||||
const parentNodes = findAllParentNodes(graph, e.item);
|
||||
// 如果没有打开过全局参数抽屉,获取不到全局参数
|
||||
const globalParams =
|
||||
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
|
||||
propsRef.current.showDrawer(e, globalParams, parentNodes);
|
||||
}
|
||||
});
|
||||
graph.on('aftercreateedge', (e) => {
|
||||
// update the sourceAnchor and targetAnchor for the newly added edge
|
||||
graph.updateItem(e.edge, {
|
||||
|
@ -603,59 +564,6 @@ const EditPipeline = () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
graph.on('node:mouseenter', (e) => {
|
||||
// this.graph.setItemState(e.item, 'showAnchors', true);
|
||||
graph.setItemState(e.item, 'nodeSelected', true);
|
||||
graph.updateItem(e.item, {
|
||||
// 节点的样式
|
||||
style: {
|
||||
stroke: '#1664ff',
|
||||
},
|
||||
});
|
||||
});
|
||||
graph.on('node:mouseleave', (e) => {
|
||||
// this.graph.setItemState(e.item, 'showAnchors', false);
|
||||
graph.setItemState(e.item, 'nodeSelected', false);
|
||||
graph.updateItem(e.item, {
|
||||
// 节点的样式
|
||||
style: {
|
||||
stroke: 'transparent',
|
||||
},
|
||||
});
|
||||
});
|
||||
graph.on('node:dragenter', (e) => {
|
||||
console.log(e.target.get('name'));
|
||||
console.log('node:dragenter');
|
||||
graph.setItemState(e.item, 'nodeSelected', true);
|
||||
graph.updateItem(e.item, {
|
||||
// 节点的样式
|
||||
style: {
|
||||
stroke: '#1664ff',
|
||||
},
|
||||
});
|
||||
});
|
||||
graph.on('node:dragleave', (e) => {
|
||||
console.log(e.target.get('name'));
|
||||
console.log('node:dragleave');
|
||||
graph.setItemState(e.item, 'nodeSelected', false);
|
||||
graph.updateItem(e.item, {
|
||||
// 节点的样式
|
||||
style: {
|
||||
stroke: 'transparent',
|
||||
},
|
||||
});
|
||||
});
|
||||
graph.on('node:dragstart', (e) => {
|
||||
console.log('node:dragstart');
|
||||
graph.setItemState(e.item, 'nodeSelected', true);
|
||||
graph.updateItem(e.item, {
|
||||
// 节点的样式
|
||||
style: {
|
||||
stroke: '#1664ff',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
graph.on('afterremoveitem', (e) => {
|
||||
if (e.item && e.item.source && e.item.target) {
|
||||
const sourceNode = graph.findById(e.item.source);
|
||||
|
@ -681,7 +589,6 @@ const EditPipeline = () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// after clicking on the first node, the edge is created, update the sourceAnchor
|
||||
graph.on('afteradditem', (e) => {
|
||||
if (e.item && e.item.getType() === 'edge') {
|
||||
|
@ -690,11 +597,29 @@ const EditPipeline = () => {
|
|||
});
|
||||
}
|
||||
});
|
||||
graph.on('node:mouseenter', (e) => {
|
||||
graph.setItemState(e.item, 'hover', true);
|
||||
});
|
||||
graph.on('node:mouseleave', (e) => {
|
||||
graph.setItemState(e.item, 'hover', false);
|
||||
});
|
||||
graph.on('node:dragenter', (e) => {
|
||||
graph.setItemState(e.item, 'hover', true);
|
||||
});
|
||||
graph.on('node:dragleave', (e) => {
|
||||
graph.setItemState(e.item, 'hover', false);
|
||||
});
|
||||
graph.on('node:dragstart', (e) => {
|
||||
graph.setItemState(e.item, 'hover', true);
|
||||
});
|
||||
graph.on('node:drag', (e) => {
|
||||
graph.setItemState(e.item, 'hover', true);
|
||||
});
|
||||
window.onresize = () => {
|
||||
if (!graph || graph.get('destroyed')) return;
|
||||
if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight)
|
||||
return;
|
||||
graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20);
|
||||
if (!graphRef.current) return;
|
||||
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
|
||||
graph.fitView();
|
||||
};
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -130,3 +130,11 @@ export function deleteDataset(id) {
|
|||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// 获取模型依赖
|
||||
export function getModelAtlasReq(data) {
|
||||
return request(`/api/mmp/modelDependency/queryModelAtlas`, {
|
||||
method: 'POST',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
@ -49,17 +50,20 @@ export const getFileListFromEvent = (e: any) => {
|
|||
});
|
||||
};
|
||||
|
||||
// 去登录页面
|
||||
/**
|
||||
* 跳转到登录页
|
||||
* @param toHome 是否跳转到首页
|
||||
*/
|
||||
export const gotoLoginPage = (toHome: boolean = true) => {
|
||||
const { pathname, search } = window.location;
|
||||
const { pathname, search } = location;
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.append('redirect', pathname + search);
|
||||
const newSearch =
|
||||
toHome && pathname !== PageEnum.LOGIN && pathname !== '/' ? '' : urlParams.toString();
|
||||
console.log('pathname', pathname);
|
||||
console.log('search', search);
|
||||
if (window.location.pathname !== PageEnum.LOGIN) {
|
||||
const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString();
|
||||
// console.log('pathname', pathname);
|
||||
// console.log('search', search);
|
||||
if (pathname !== PageEnum.LOGIN) {
|
||||
closeAllModals();
|
||||
removeAllPageCacheState();
|
||||
history.replace({
|
||||
pathname: PageEnum.LOGIN,
|
||||
search: newSearch,
|
||||
|
|
Loading…
Reference in New Issue