mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-11 12:07:42 +00:00
Merge pull request #3 from sevenliu1896/feature/tristanliu
[improvement](semantic-fe) add dimension&datasource relation graph an…
This commit is contained in:
@@ -40,7 +40,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { currentUser = {} } = initialState as any;
|
const { currentUser = {} } = initialState as any;
|
||||||
console.log(currentUser, 'currentUser');
|
|
||||||
const menuHeaderDropdown = (
|
const menuHeaderDropdown = (
|
||||||
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
|
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
|
||||||
<Menu.Item key="logout">
|
<Menu.Item key="logout">
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ interface Props extends AvatarProps {
|
|||||||
staffName?: string;
|
staffName?: string;
|
||||||
avatarImg?: string;
|
avatarImg?: string;
|
||||||
}
|
}
|
||||||
|
const { tmeAvatarUrl } = process.env;
|
||||||
const TMEAvatar: FC<Props> = ({ avatarImg, ...restProps }) => (
|
const TMEAvatar: FC<Props> = ({ staffName, avatarImg, ...restProps }) => {
|
||||||
<Avatar src={`${avatarImg}`} alt="avatar" icon={<img src={avatarIcon} />} {...restProps} />
|
const avatarSrc = tmeAvatarUrl ? `${tmeAvatarUrl}${staffName}.png` : avatarImg;
|
||||||
);
|
return (
|
||||||
|
<Avatar src={`${avatarSrc}`} alt="avatar" icon={<img src={avatarIcon} />} {...restProps} />
|
||||||
|
);
|
||||||
|
};
|
||||||
export default TMEAvatar;
|
export default TMEAvatar;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { postUserLogin, userRegister } from './services';
|
|||||||
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
import { AUTH_TOKEN_KEY } from '@/common/constants';
|
||||||
import { queryCurrentUser } from '@/services/user';
|
import { queryCurrentUser } from '@/services/user';
|
||||||
import { history, useModel } from 'umi';
|
import { history, useModel } from 'umi';
|
||||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
// import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||||
|
|
||||||
const { Item } = Form;
|
const { Item } = Form;
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
@@ -25,7 +25,7 @@ const LoginPage: React.FC = () => {
|
|||||||
const { code, data, msg } = await postUserLogin(values);
|
const { code, data, msg } = await postUserLogin(values);
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
localStorage.setItem(AUTH_TOKEN_KEY, data);
|
localStorage.setItem(AUTH_TOKEN_KEY, data);
|
||||||
setChatSdkToken(data || '');
|
// setChatSdkToken(data || '');
|
||||||
const { code: queryUserCode, data: queryUserData } = await queryCurrentUser();
|
const { code: queryUserCode, data: queryUserData } = await queryCurrentUser();
|
||||||
if (queryUserCode === 200) {
|
if (queryUserCode === 200) {
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
|
|||||||
@@ -167,7 +167,12 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
|
|||||||
disabled={!editState}
|
disabled={!editState}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
handleFieldChange(record, 'name', value);
|
// handleFieldChange(record, 'name', value);
|
||||||
|
onFieldChange(record.bizName, {
|
||||||
|
...record,
|
||||||
|
name: value,
|
||||||
|
[isCreateName]: 1,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="请输入中文名"
|
placeholder="请输入中文名"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,82 +1,140 @@
|
|||||||
import G6 from '@antv/g6';
|
import G6 from '@antv/g6';
|
||||||
// import { modifyCSS, createDom } from '@antv/dom-util';
|
|
||||||
import { createDom } from '@antv/dom-util';
|
import { createDom } from '@antv/dom-util';
|
||||||
const initToolBar = () => {
|
|
||||||
// const defaultConfig = G6.ToolBar
|
|
||||||
const toolBarInstance = new G6.ToolBar();
|
|
||||||
|
|
||||||
|
const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
|
||||||
|
|
||||||
|
const searchNode = (graph) => {
|
||||||
|
const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
|
||||||
|
const searchText = toolBarSearchInput.value.trim();
|
||||||
|
let lastFoundNode = null;
|
||||||
|
graph.getNodes().forEach((node) => {
|
||||||
|
const model = node.getModel();
|
||||||
|
const isFound = searchText && model.label.includes(searchText);
|
||||||
|
if (isFound) {
|
||||||
|
graph.setItemState(node, 'active', true);
|
||||||
|
lastFoundNode = node;
|
||||||
|
} else {
|
||||||
|
graph.setItemState(node, 'active', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastFoundNode) {
|
||||||
|
// 将视图移动到找到的节点位置
|
||||||
|
graph.focusItem(lastFoundNode, true, {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'easeCubic',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatorSearchInputDom = (graph) => {
|
||||||
|
const domString =
|
||||||
|
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
|
||||||
|
const searchInputDom = createDom(domString);
|
||||||
|
searchInputDom.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
searchNode(graph);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return searchInputDom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatorSearchBtnDom = (graph) => {
|
||||||
|
const domString = `<button
|
||||||
|
id="toolBarSearchBtn"
|
||||||
|
type="button"
|
||||||
|
class="ant-btn ant-btn-default ant-btn-icon-only ant-input-search-button"
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="search" class="anticon anticon-search">
|
||||||
|
<svg
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="search"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
${searchIconSvgPath}
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>`;
|
||||||
|
const searchBtnDom = createDom(domString);
|
||||||
|
searchBtnDom.addEventListener('click', () => {
|
||||||
|
searchNode(graph);
|
||||||
|
});
|
||||||
|
return searchBtnDom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchInputDOM = (graph) => {
|
||||||
|
const searchInputDom = generatorSearchInputDom(graph);
|
||||||
|
const searchBtnDom = generatorSearchBtnDom(graph);
|
||||||
|
const searchInput = `
|
||||||
|
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;display:none">
|
||||||
|
<span class="ant-input-group-wrapper ant-input-search" >
|
||||||
|
<span class="ant-input-wrapper ant-input-group" id="toolBarSearchWrapper">
|
||||||
|
<span class="ant-input-group-addon"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
const searchDom = createDom(searchInput);
|
||||||
|
const searchWrapperDom = searchDom.querySelector('#toolBarSearchWrapper');
|
||||||
|
searchWrapperDom.insertBefore(searchInputDom, searchWrapperDom.firstChild);
|
||||||
|
searchWrapperDom.querySelector('.ant-input-group-addon').appendChild(searchBtnDom);
|
||||||
|
return searchDom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initToolBar = () => {
|
||||||
|
const toolBarInstance = new G6.ToolBar();
|
||||||
const config = toolBarInstance._cfgs;
|
const config = toolBarInstance._cfgs;
|
||||||
const defaultContentDomString = config.getContent();
|
const defaultContentDomString = config.getContent();
|
||||||
// const regex = /<ul[^>]*>|<\/ul>/g;
|
|
||||||
// const innerDom = defaultContentDom.replace(regex, '');
|
|
||||||
const defaultContentDom = createDom(defaultContentDomString);
|
const defaultContentDom = createDom(defaultContentDomString);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const elements = defaultContentDom.querySelectorAll('li[code="redo"], li[code="undo"]');
|
const elements = defaultContentDom.querySelectorAll('li[code="redo"], li[code="undo"]');
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchBtnDom = `<li code="search">
|
const searchBtnDom = `<li code="search">
|
||||||
<svg
|
<svg
|
||||||
viewBox="64 64 896 896"
|
viewBox="64 64 896 896"
|
||||||
focusable="false"
|
class="icon"
|
||||||
data-icon="search"
|
data-icon="search"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />
|
${searchIconSvgPath}
|
||||||
</svg>
|
</svg>
|
||||||
</li>`;
|
</li>`;
|
||||||
|
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
|
||||||
|
let searchInputContentVisible = false;
|
||||||
const toolbar = new G6.ToolBar({
|
const toolbar = new G6.ToolBar({
|
||||||
position: { x: 10, y: 10 },
|
position: { x: 10, y: 10 },
|
||||||
getContent: () => {
|
className: 'semantic-graph-toolbar',
|
||||||
return `${searchBtnDom}${defaultContentDom}`;
|
getContent: (graph) => {
|
||||||
|
const searchInput = searchInputDOM(graph);
|
||||||
|
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
|
||||||
|
const contentDom = createDom(content);
|
||||||
|
contentDom.appendChild(searchInput);
|
||||||
|
return contentDom;
|
||||||
|
},
|
||||||
|
handleClick: (code, graph) => {
|
||||||
|
if (code === 'search') {
|
||||||
|
const searchText = document.getElementById('searchInputContent');
|
||||||
|
if (searchText) {
|
||||||
|
const visible = searchInputContentVisible ? 'none' : 'block';
|
||||||
|
searchText.style.display = visible;
|
||||||
|
searchInputContentVisible = !searchInputContentVisible;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handleDefaultOperator public方法缺失graph作为参数传入,将graph挂载在cfgs上,源码通过get会获取到graph,完成默认code的执行逻辑
|
||||||
|
toolBarInstance._cfgs.graph = graph;
|
||||||
|
toolBarInstance.handleDefaultOperator(code);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// const toolbar = new G6.ToolBar({
|
|
||||||
// getContent: (graph) => {
|
|
||||||
// const searchInput = document.createElement('input');
|
|
||||||
// searchInput.id = 'search-input';
|
|
||||||
// searchInput.placeholder = '搜索节点';
|
|
||||||
|
|
||||||
// const searchBtn = document.createElement('button');
|
|
||||||
// searchBtn.id = 'search-btn';
|
|
||||||
// searchBtn.innerHTML = '搜索';
|
|
||||||
|
|
||||||
// const container = document.createElement('div');
|
|
||||||
// container.appendChild(searchInput);
|
|
||||||
// container.appendChild(searchBtn);
|
|
||||||
// return container;
|
|
||||||
// },
|
|
||||||
// handleClick: (name, graph) => {
|
|
||||||
// if (name === 'search-btn') {
|
|
||||||
// const searchText = document.getElementById('search-input').value.trim();
|
|
||||||
// if (!searchText) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const foundNode = graph.getNodes().find((node) => {
|
|
||||||
// const model = node.getModel();
|
|
||||||
// return model.label === searchText;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (foundNode) {
|
|
||||||
// // 如果找到了节点,将其设置为选中状态
|
|
||||||
// graph.setItemState(foundNode, 'active', true);
|
|
||||||
// // 将视图移动到找到的节点位置
|
|
||||||
// graph.focusItem(foundNode, true, {
|
|
||||||
// duration: 300,
|
|
||||||
// easing: 'easeCubic',
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// alert('未找到节点');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
return toolbar;
|
return toolbar;
|
||||||
};
|
};
|
||||||
export default initToolBar;
|
export default initToolBar;
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import G6 from '@antv/g6';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const initTooltips = () => {
|
||||||
|
const tooltip = new G6.Tooltip({
|
||||||
|
offsetX: 10,
|
||||||
|
offsetY: 10,
|
||||||
|
fixToNode: [1, 0.5],
|
||||||
|
// the types of items that allow the tooltip show up
|
||||||
|
// 允许出现 tooltip 的 item 类型
|
||||||
|
// itemTypes: ['node', 'edge'],
|
||||||
|
itemTypes: ['node'],
|
||||||
|
// custom the tooltip's content
|
||||||
|
// 自定义 tooltip 内容
|
||||||
|
getContent: (e) => {
|
||||||
|
const outDiv = document.createElement('div');
|
||||||
|
outDiv.style.width = 'fit-content';
|
||||||
|
outDiv.style.height = 'fit-content';
|
||||||
|
const model = e.item.getModel();
|
||||||
|
console.log(model, e.item, 'model');
|
||||||
|
const { name, bizName, createdBy, updatedAt, description } = model;
|
||||||
|
const list = [
|
||||||
|
{
|
||||||
|
label: '名称:',
|
||||||
|
value: name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '字段:',
|
||||||
|
value: bizName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建人:',
|
||||||
|
value: createdBy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '更新时间:',
|
||||||
|
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '描述:',
|
||||||
|
value: description,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const listHtml = list.reduce((htmlString, item) => {
|
||||||
|
const { label, value } = item;
|
||||||
|
if (value) {
|
||||||
|
htmlString += `<p style="margin-bottom:0">
|
||||||
|
<span>${label} </span>
|
||||||
|
<span>${value}</span>
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
return htmlString;
|
||||||
|
}, '');
|
||||||
|
const html = `<div>
|
||||||
|
${listHtml}
|
||||||
|
</div>`;
|
||||||
|
if (e.item.getType() === 'node') {
|
||||||
|
outDiv.innerHTML = html;
|
||||||
|
}
|
||||||
|
// else {
|
||||||
|
// const source = e.item.getSource();
|
||||||
|
// const target = e.item.getTarget();
|
||||||
|
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
|
||||||
|
// target.getModel().name
|
||||||
|
// }`;
|
||||||
|
// }
|
||||||
|
return outDiv;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return tooltip;
|
||||||
|
};
|
||||||
|
export default initTooltips;
|
||||||
@@ -3,9 +3,10 @@ import { connect } from 'umi';
|
|||||||
import type { StateType } from '../model';
|
import type { StateType } from '../model';
|
||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
import { typeConfigs } from './utils';
|
import { typeConfigs } from './utils';
|
||||||
import { message } from 'antd';
|
import { message, Row, Col, Radio } from 'antd';
|
||||||
import { getDatasourceList, getDomainSchemaRela } from '../service';
|
import { getDatasourceList, getDomainSchemaRela } from '../service';
|
||||||
import initToolBar from './components/ToolBar';
|
import initToolBar from './components/ToolBar';
|
||||||
|
import initTooltips from './components/ToolTips';
|
||||||
import G6 from '@antv/g6';
|
import G6 from '@antv/g6';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -17,11 +18,13 @@ type Props = {
|
|||||||
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [graphData, setGraphData] = useState<any>({});
|
const [graphData, setGraphData] = useState<any>({});
|
||||||
|
const [dataSourceListData, setDataSourceListData] = useState<any[]>([]);
|
||||||
|
const [graphShowType, setGraphShowType] = useState<string>('dimension');
|
||||||
const legendDataRef = useRef<any[]>([]);
|
const legendDataRef = useRef<any[]>([]);
|
||||||
const graphRef = useRef<any>(null);
|
const graphRef = useRef<any>(null);
|
||||||
const legendDataFilterFunctions = useRef<any>({});
|
const legendDataFilterFunctions = useRef<any>({});
|
||||||
|
|
||||||
const { dimensionList } = domainManger;
|
// const { dimensionList } = domainManger;
|
||||||
|
|
||||||
const toggleNodeVisibility = (graph, node, visible) => {
|
const toggleNodeVisibility = (graph, node, visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -42,37 +45,80 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatterRelationData = (dataSourceList: any[]) => {
|
const getDimensionChildren = (dimensions: any[], dataSourceId: string) => {
|
||||||
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
|
const dimensionChildrenList = dimensions.reduce((dimensionChildren: any[], dimension: any) => {
|
||||||
const { id, name } = item;
|
const {
|
||||||
const dataSourceId = `dataSource-${id}`;
|
id: dimensionId,
|
||||||
const dimensionChildrenList = dimensionList.reduce(
|
name: dimensionName,
|
||||||
(dimensionChildren: any[], dimension: any) => {
|
bizName,
|
||||||
const { id: dimensionId, name: dimensionName, datasourceId } = dimension;
|
description,
|
||||||
if (datasourceId === id) {
|
createdBy,
|
||||||
dimensionChildren.push({
|
updatedAt,
|
||||||
nodeType: 'dimension',
|
} = dimension;
|
||||||
legendType: dataSourceId,
|
// if (datasourceId === id) {
|
||||||
id: `dimension-${dimensionId}`,
|
dimensionChildren.push({
|
||||||
name: dimensionName,
|
nodeType: 'dimension',
|
||||||
style: {
|
legendType: dataSourceId,
|
||||||
lineWidth: 2,
|
id: `dimension-${dimensionId}`,
|
||||||
fill: '#f0f7ff',
|
name: dimensionName,
|
||||||
stroke: '#a6ccff',
|
bizName,
|
||||||
},
|
description,
|
||||||
});
|
createdBy,
|
||||||
}
|
updatedAt,
|
||||||
return dimensionChildren;
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#f0f7ff',
|
||||||
|
stroke: '#a6ccff',
|
||||||
},
|
},
|
||||||
[],
|
});
|
||||||
);
|
// }
|
||||||
|
return dimensionChildren;
|
||||||
|
}, []);
|
||||||
|
return dimensionChildrenList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMetricChildren = (metrics: any[], dataSourceId: string) => {
|
||||||
|
const metricsChildrenList = metrics.reduce((metricsChildren: any[], dimension: any) => {
|
||||||
|
const { id, name, bizName, description, createdBy, updatedAt } = dimension;
|
||||||
|
metricsChildren.push({
|
||||||
|
nodeType: 'metric',
|
||||||
|
legendType: dataSourceId,
|
||||||
|
id: `dimension-${id}`,
|
||||||
|
name,
|
||||||
|
bizName,
|
||||||
|
description,
|
||||||
|
createdBy,
|
||||||
|
updatedAt,
|
||||||
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
fill: '#f0f7ff',
|
||||||
|
stroke: '#a6ccff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return metricsChildren;
|
||||||
|
}, []);
|
||||||
|
return metricsChildrenList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatterRelationData = (dataSourceList: any[], type = graphShowType) => {
|
||||||
|
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
|
||||||
|
const { datasource, dimensions, metrics } = item;
|
||||||
|
const { id, name } = datasource;
|
||||||
|
const dataSourceId = `dataSource-${id}`;
|
||||||
|
let childrenList = [];
|
||||||
|
if (type === 'metirc') {
|
||||||
|
childrenList = getMetricChildren(metrics, dataSourceId);
|
||||||
|
}
|
||||||
|
if (type === 'dimension') {
|
||||||
|
childrenList = getDimensionChildren(dimensions, dataSourceId);
|
||||||
|
}
|
||||||
relationList.push({
|
relationList.push({
|
||||||
name,
|
name,
|
||||||
legendType: dataSourceId,
|
legendType: dataSourceId,
|
||||||
id: dataSourceId,
|
id: dataSourceId,
|
||||||
nodeType: 'datasource',
|
nodeType: 'datasource',
|
||||||
size: 40,
|
size: 40,
|
||||||
children: [...dimensionChildrenList],
|
children: [...childrenList],
|
||||||
style: {
|
style: {
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
fill: '#BDEFDB',
|
fill: '#BDEFDB',
|
||||||
@@ -84,26 +130,33 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
return relationData;
|
return relationData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeGraphData = (data: any, type?: string) => {
|
||||||
|
const relationData = formatterRelationData(data, type);
|
||||||
|
const legendList = relationData.map((item: any) => {
|
||||||
|
const { id, name } = item;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label: name,
|
||||||
|
order: 4,
|
||||||
|
...typeConfigs.datasource,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
legendDataRef.current = legendList;
|
||||||
|
const graphRootData = {
|
||||||
|
id: 'root',
|
||||||
|
name: domainManger.selectDomainName,
|
||||||
|
children: relationData,
|
||||||
|
};
|
||||||
|
setGraphData(graphRootData);
|
||||||
|
};
|
||||||
|
|
||||||
const queryDataSourceList = async (params: any) => {
|
const queryDataSourceList = async (params: any) => {
|
||||||
getDomainSchemaRela(params.domainId);
|
const { code, data, msg } = await getDomainSchemaRela(params.domainId);
|
||||||
const { code, data, msg } = await getDatasourceList({ ...params });
|
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
const relationData = formatterRelationData(data);
|
if (data) {
|
||||||
const legendList = relationData.map((item: any) => {
|
changeGraphData(data);
|
||||||
const { id, name } = item;
|
setDataSourceListData(data);
|
||||||
return {
|
}
|
||||||
id,
|
|
||||||
label: name,
|
|
||||||
order: 4,
|
|
||||||
...typeConfigs.datasource,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
legendDataRef.current = legendList;
|
|
||||||
setGraphData({
|
|
||||||
id: 'root',
|
|
||||||
name: domainManger.selectDomainName,
|
|
||||||
children: relationData,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
}
|
}
|
||||||
@@ -111,7 +164,7 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queryDataSourceList({ domainId });
|
queryDataSourceList({ domainId });
|
||||||
}, []);
|
}, [domainId]);
|
||||||
|
|
||||||
const getLegendDataFilterFunctions = () => {
|
const getLegendDataFilterFunctions = () => {
|
||||||
legendDataRef.current.map((item: any) => {
|
legendDataRef.current.map((item: any) => {
|
||||||
@@ -143,201 +196,214 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
|
|||||||
};
|
};
|
||||||
// const [visible, setVisible] = useState(false);
|
// const [visible, setVisible] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(domainId, graphData, 'domainId');
|
||||||
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
|
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById('semanticGraph');
|
const container = document.getElementById('semanticGraph');
|
||||||
const width = container!.scrollWidth;
|
const width = container!.scrollWidth;
|
||||||
const height = container!.scrollHeight || 500;
|
const height = container!.scrollHeight || 500;
|
||||||
|
|
||||||
if (!graphRef.current) {
|
// if (!graphRef.current) {
|
||||||
getLegendDataFilterFunctions();
|
getLegendDataFilterFunctions();
|
||||||
|
|
||||||
const toolbar = initToolBar();
|
const toolbar = initToolBar();
|
||||||
// const toolbar = new G6.ToolBar({
|
const tooltip = initTooltips();
|
||||||
// getContent: (graph) => {
|
const legend = new G6.Legend({
|
||||||
// const searchIcon = document.createElement('i');
|
// container: 'legendContainer',
|
||||||
// searchIcon.className = 'g6-toolbar-search-icon';
|
data: {
|
||||||
// searchIcon.style.cssText = `
|
nodes: legendDataRef.current,
|
||||||
// display: inline-block;
|
},
|
||||||
// width: 16px;
|
align: 'center',
|
||||||
// height: 16px;
|
layout: 'horizontal', // vertical
|
||||||
// background-image: url(https://gw.alipayobjects.com/zos/rmsportal/wzQIcOMRTkQwFgaaDIFs.svg);
|
position: 'bottom-right',
|
||||||
// background-size: 16px 16px;
|
vertiSep: 12,
|
||||||
// margin-right: 8px;
|
horiSep: 24,
|
||||||
// cursor: pointer;
|
offsetY: -24,
|
||||||
// `;
|
padding: [4, 16, 8, 16],
|
||||||
|
containerStyle: {
|
||||||
// searchIcon.addEventListener('click', () => {
|
fill: '#ccc',
|
||||||
// setVisible((prevVisible) => !prevVisible);
|
lineWidth: 1,
|
||||||
// });
|
},
|
||||||
|
title: '可见数据源',
|
||||||
// const ul = document.createElement('ul');
|
titleConfig: {
|
||||||
// ul.className = 'g6-component-toolbar';
|
position: 'center',
|
||||||
// ul.appendChild(searchIcon);
|
offsetX: 0,
|
||||||
|
offsetY: 12,
|
||||||
// return ul;
|
style: {
|
||||||
// },
|
fontSize: 12,
|
||||||
// });
|
fontWeight: 500,
|
||||||
|
fill: '#000',
|
||||||
const tooltip = new G6.Tooltip({
|
|
||||||
offsetX: 10,
|
|
||||||
offsetY: 10,
|
|
||||||
fixToNode: [1, 0.5],
|
|
||||||
// the types of items that allow the tooltip show up
|
|
||||||
// 允许出现 tooltip 的 item 类型
|
|
||||||
// itemTypes: ['node', 'edge'],
|
|
||||||
itemTypes: ['node'],
|
|
||||||
// custom the tooltip's content
|
|
||||||
// 自定义 tooltip 内容
|
|
||||||
getContent: (e) => {
|
|
||||||
const outDiv = document.createElement('div');
|
|
||||||
outDiv.style.width = 'fit-content';
|
|
||||||
outDiv.style.height = 'fit-content';
|
|
||||||
const model = e.item.getModel();
|
|
||||||
if (e.item.getType() === 'node') {
|
|
||||||
outDiv.innerHTML = `${model.name}`;
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// const source = e.item.getSource();
|
|
||||||
// const target = e.item.getTarget();
|
|
||||||
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
|
|
||||||
// target.getModel().name
|
|
||||||
// }`;
|
|
||||||
// }
|
|
||||||
return outDiv;
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const legend = new G6.Legend({
|
filter: {
|
||||||
// container: 'legendContainer',
|
enable: true,
|
||||||
data: {
|
multiple: true,
|
||||||
nodes: legendDataRef.current,
|
trigger: 'click',
|
||||||
|
graphActiveState: 'activeByLegend',
|
||||||
|
graphInactiveState: 'inactiveByLegend',
|
||||||
|
filterFunctions: {
|
||||||
|
...legendDataFilterFunctions.current,
|
||||||
},
|
},
|
||||||
align: 'center',
|
},
|
||||||
layout: 'horizontal', // vertical
|
});
|
||||||
position: 'bottom-right',
|
// 我使用TreeGraph进行layout布局,采用{type: 'compactBox',direction: 'LR'}模式,如何使子节点与根节点的连线只连接到上下连接桩上
|
||||||
vertiSep: 12,
|
|
||||||
horiSep: 24,
|
graphRef.current = new G6.TreeGraph({
|
||||||
offsetY: -24,
|
container: 'semanticGraph',
|
||||||
padding: [4, 16, 8, 16],
|
width,
|
||||||
containerStyle: {
|
height,
|
||||||
fill: '#ccc',
|
linkCenter: true,
|
||||||
lineWidth: 1,
|
modes: {
|
||||||
},
|
default: [
|
||||||
title: '可见数据源',
|
{
|
||||||
titleConfig: {
|
type: 'collapse-expand',
|
||||||
position: 'center',
|
onChange: function onChange(item, collapsed) {
|
||||||
offsetX: 0,
|
const data = item.get('model');
|
||||||
offsetY: 12,
|
data.collapsed = collapsed;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'drag-node',
|
||||||
|
'drag-canvas',
|
||||||
|
// 'activate-relations',
|
||||||
|
'zoom-canvas',
|
||||||
|
{
|
||||||
|
type: 'activate-relations',
|
||||||
|
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
|
||||||
|
resetSelected: true, // 点击空白处时,是否取消高亮
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultNode: {
|
||||||
|
size: 26,
|
||||||
|
labelCfg: {
|
||||||
|
position: 'right',
|
||||||
|
offset: 5,
|
||||||
style: {
|
style: {
|
||||||
fontSize: 12,
|
stroke: '#fff',
|
||||||
fontWeight: 500,
|
lineWidth: 4,
|
||||||
fill: '#000',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filter: {
|
},
|
||||||
enable: true,
|
defaultEdge: {
|
||||||
multiple: true,
|
type: 'cubic-horizontal',
|
||||||
trigger: 'click',
|
// type: 'flow-line',
|
||||||
graphActiveState: 'activeByLegend',
|
// type: 'polyline',
|
||||||
graphInactiveState: 'inactiveByLegend',
|
// type: 'line',
|
||||||
filterFunctions: {
|
/* configure the bending radius and min distance to the end nodes */
|
||||||
...legendDataFilterFunctions.current,
|
style: {
|
||||||
},
|
radius: 10,
|
||||||
|
offset: 30,
|
||||||
|
endArrow: true,
|
||||||
|
/* and other styles */
|
||||||
|
// stroke: '#F6BD16',
|
||||||
},
|
},
|
||||||
});
|
// style: {
|
||||||
|
// stroke: '#A3B1BF',
|
||||||
graphRef.current = new G6.TreeGraph({
|
// },
|
||||||
container: 'semanticGraph',
|
},
|
||||||
width,
|
layout: {
|
||||||
height,
|
type: 'mindmap',
|
||||||
linkCenter: true,
|
direction: 'H',
|
||||||
modes: {
|
getId: function getId(d) {
|
||||||
default: [
|
return d.id;
|
||||||
{
|
|
||||||
type: 'collapse-expand',
|
|
||||||
onChange: function onChange(item, collapsed) {
|
|
||||||
const data = item.get('model');
|
|
||||||
data.collapsed = collapsed;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'drag-node',
|
|
||||||
'drag-canvas',
|
|
||||||
// 'activate-relations',
|
|
||||||
'zoom-canvas',
|
|
||||||
{
|
|
||||||
type: 'activate-relations',
|
|
||||||
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
|
|
||||||
resetSelected: true, // 点击空白处时,是否取消高亮
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
defaultNode: {
|
getHeight: function getHeight() {
|
||||||
size: 26,
|
return 16;
|
||||||
labelCfg: {
|
|
||||||
position: 'bottom',
|
|
||||||
style: {
|
|
||||||
stroke: '#fff',
|
|
||||||
lineWidth: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
getWidth: function getWidth() {
|
||||||
layout: {
|
return 16;
|
||||||
type: 'dendrogram',
|
|
||||||
direction: 'LR',
|
|
||||||
nodeSep: 200,
|
|
||||||
rankSep: 300,
|
|
||||||
radial: true,
|
|
||||||
},
|
},
|
||||||
plugins: [legend, tooltip, toolbar],
|
getVGap: function getVGap() {
|
||||||
});
|
return 30;
|
||||||
|
},
|
||||||
|
getHGap: function getHGap() {
|
||||||
|
return 100;
|
||||||
|
},
|
||||||
|
// type: 'dendrogram',
|
||||||
|
// direction: 'LR',
|
||||||
|
// nodeSep: 200,
|
||||||
|
// rankSep: 300,
|
||||||
|
// radial: true,
|
||||||
|
},
|
||||||
|
plugins: [legend, tooltip, toolbar],
|
||||||
|
});
|
||||||
|
|
||||||
const legendCanvas = legend._cfgs.legendCanvas;
|
const legendCanvas = legend._cfgs.legendCanvas;
|
||||||
|
|
||||||
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑,在注册click事件前,先将click事件队列清空;
|
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑,在注册click事件前,先将click事件队列清空;
|
||||||
legend._cfgs.legendCanvas._events.click = [];
|
legend._cfgs.legendCanvas._events.click = [];
|
||||||
legendCanvas.on('click', (e) => {
|
legendCanvas.on('click', (e) => {
|
||||||
const shape = e.target;
|
const shape = e.target;
|
||||||
const shapeGroup = shape.get('parent');
|
const shapeGroup = shape.get('parent');
|
||||||
const shapeGroupId = shapeGroup?.cfg?.id;
|
const shapeGroupId = shapeGroup?.cfg?.id;
|
||||||
if (shapeGroupId) {
|
if (shapeGroupId) {
|
||||||
const isActive = shapeGroup.get('active');
|
const isActive = shapeGroup.get('active');
|
||||||
const targetNode = graph.findById(shapeGroupId);
|
const targetNode = graph.findById(shapeGroupId);
|
||||||
// const model = targetNode.getModel();
|
// const model = targetNode.getModel();
|
||||||
toggleNodeVisibility(graph, targetNode, isActive);
|
toggleNodeVisibility(graph, targetNode, isActive);
|
||||||
toggleChildrenVisibility(graph, targetNode, isActive);
|
toggleChildrenVisibility(graph, targetNode, isActive);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current;
|
||||||
|
|
||||||
graph.node(function (node) {
|
graph.node(function (node) {
|
||||||
return {
|
return {
|
||||||
label: node.name,
|
label: node.name,
|
||||||
labelCfg: { style: { fill: '#3c3c3c' } },
|
labelCfg: { style: { fill: '#3c3c3c' } },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
console.log(graphData, 'graphData');
|
||||||
|
// graph.data(graphData);
|
||||||
|
graph.changeData(graphData);
|
||||||
|
graph.render();
|
||||||
|
graph.fitView();
|
||||||
|
|
||||||
graph.data(graphData);
|
setAllActiveLegend(legend);
|
||||||
graph.render();
|
|
||||||
graph.fitView();
|
|
||||||
|
|
||||||
setAllActiveLegend(legend);
|
const rootNode = graph.findById('root');
|
||||||
|
graph.hideItem(rootNode);
|
||||||
|
if (typeof window !== 'undefined')
|
||||||
|
window.onresize = () => {
|
||||||
|
if (!graph || graph.get('destroyed')) return;
|
||||||
|
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
||||||
|
graph.changeSize(container.scrollWidth, container.scrollHeight);
|
||||||
|
};
|
||||||
|
// }
|
||||||
|
}, [graphData]);
|
||||||
|
|
||||||
const rootNode = graph.findById('root');
|
return (
|
||||||
graph.hideItem(rootNode);
|
<>
|
||||||
if (typeof window !== 'undefined')
|
<Row>
|
||||||
window.onresize = () => {
|
<Col flex="auto" />
|
||||||
if (!graph || graph.get('destroyed')) return;
|
<Col flex="100px">
|
||||||
if (!container || !container.scrollWidth || !container.scrollHeight) return;
|
<Radio.Group
|
||||||
graph.changeSize(container.scrollWidth, container.scrollHeight);
|
buttonStyle="solid"
|
||||||
};
|
size="small"
|
||||||
}
|
value={graphShowType}
|
||||||
}, [domainId, graphData]);
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
return <div ref={ref} id="semanticGraph" style={{ width: '100%', height: '100%' }} />;
|
console.log(value, 'value');
|
||||||
|
setGraphShowType(value);
|
||||||
|
changeGraphData(dataSourceListData, value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio.Button value="dimension">维度</Radio.Button>
|
||||||
|
<Radio.Button value="metric">指标</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
key={`${domainId}-${graphShowType}`}
|
||||||
|
id="semanticGraph"
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
export default connect(({ domainManger }: { domainManger: StateType }) => ({
|
||||||
domainManger,
|
domainManger,
|
||||||
|
|||||||
Reference in New Issue
Block a user