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

This commit is contained in:
西大锐 2024-06-07 11:39:22 +08:00
commit a581c37596
30 changed files with 1174 additions and 334 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

@ -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: [

View File

@ -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]);
};

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';
@ -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>
);

View File

@ -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}`);
};
// 分页切换

View File

@ -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" />}>

View File

@ -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;

View File

@ -22,4 +22,3 @@ function DatasetAnnotation() {
}
export default DatasetAnnotation;

View File

@ -99,7 +99,10 @@ function LogGroup({
scrollToBottom();
}, 100);
}
} else {
}
// 判断是否日志是否加载完成
if (!log_detail?.log_content) {
setCompleted(true);
}
};

View File

@ -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 (

View File

@ -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" />}

View File

@ -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,

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,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;

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,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;

View File

@ -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}`;

View File

@ -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;

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

@ -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 (

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';
@ -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,