Files
supersonic/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx

408 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'umi';
import type { StateType } from '../model';
import type { Dispatch } from 'umi';
import { typeConfigs } from './utils';
import { message, Row, Col, Radio } from 'antd';
import { getDatasourceList, getDomainSchemaRela } from '../service';
import initToolBar from './components/ToolBar';
import initTooltips from './components/ToolTips';
import G6 from '@antv/g6';
type Props = {
domainId: number;
domainManger: StateType;
dispatch: Dispatch;
};
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
const ref = useRef(null);
const [graphData, setGraphData] = useState<any>({});
const [dataSourceListData, setDataSourceListData] = useState<any[]>([]);
const [graphShowType, setGraphShowType] = useState<string>('dimension');
const legendDataRef = useRef<any[]>([]);
const graphRef = useRef<any>(null);
const legendDataFilterFunctions = useRef<any>({});
// const { dimensionList } = domainManger;
const toggleNodeVisibility = (graph, node, visible) => {
if (visible) {
graph.showItem(node);
} else {
graph.hideItem(node);
}
};
const toggleChildrenVisibility = (graph, node, visible) => {
const model = node.getModel();
if (model.children) {
model.children.forEach((child) => {
const childNode = graph.findById(child.id);
toggleNodeVisibility(graph, childNode, visible);
toggleChildrenVisibility(graph, childNode, visible);
});
}
};
const getDimensionChildren = (dimensions: any[], dataSourceId: string) => {
const dimensionChildrenList = dimensions.reduce((dimensionChildren: any[], dimension: any) => {
const {
id: dimensionId,
name: dimensionName,
bizName,
description,
createdBy,
updatedAt,
} = dimension;
// if (datasourceId === id) {
dimensionChildren.push({
nodeType: 'dimension',
legendType: dataSourceId,
id: `dimension-${dimensionId}`,
name: dimensionName,
bizName,
description,
createdBy,
updatedAt,
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({
name,
legendType: dataSourceId,
id: dataSourceId,
nodeType: 'datasource',
size: 40,
children: [...childrenList],
style: {
lineWidth: 2,
fill: '#BDEFDB',
stroke: '#5AD8A6',
},
});
return relationList;
}, []);
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 { code, data, msg } = await getDomainSchemaRela(params.domainId);
if (code === 200) {
if (data) {
changeGraphData(data);
setDataSourceListData(data);
}
} else {
message.error(msg);
}
};
useEffect(() => {
queryDataSourceList({ domainId });
}, [domainId]);
const getLegendDataFilterFunctions = () => {
legendDataRef.current.map((item: any) => {
const { id } = item;
legendDataFilterFunctions.current = {
...legendDataFilterFunctions.current,
[id]: (d) => {
if (d.legendType === id) {
return true;
}
return false;
},
};
});
};
const setAllActiveLegend = (legend: any) => {
const legendCanvas = legend._cfgs.legendCanvas;
// 从图例中找出node-group节点;
const group = legendCanvas.find((e: any) => e.get('name') === 'node-group');
// 数据源的图例节点在node-group中的children中
const groups = group.get('children');
groups.forEach((itemGroup: any) => {
const labelText = itemGroup.find((e: any) => e.get('name') === 'circle-node-text');
// legend中activateLegend事件触发在图例节点的Text上方法中存在向上溯源的逻辑const shapeGroup = shape.get('parent');
// 因此复用实例方法时在这里不能直接将图例节点传入需要在节点的children中找任意一个元素作为入参
legend.activateLegend(labelText);
});
};
// const [visible, setVisible] = useState(false);
useEffect(() => {
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
return;
}
const container = document.getElementById('semanticGraph');
const width = container!.scrollWidth;
const height = container!.scrollHeight || 500;
// if (!graphRef.current) {
getLegendDataFilterFunctions();
const toolbar = initToolBar();
const tooltip = initTooltips();
const legend = new G6.Legend({
// container: 'legendContainer',
data: {
nodes: legendDataRef.current,
},
align: 'center',
layout: 'horizontal', // vertical
position: 'bottom-right',
vertiSep: 12,
horiSep: 24,
offsetY: -24,
padding: [4, 16, 8, 16],
containerStyle: {
fill: '#ccc',
lineWidth: 1,
},
title: '可见数据源',
titleConfig: {
position: 'center',
offsetX: 0,
offsetY: 12,
style: {
fontSize: 12,
fontWeight: 500,
fill: '#000',
},
},
filter: {
enable: true,
multiple: true,
trigger: 'click',
graphActiveState: 'activeByLegend',
graphInactiveState: 'inactiveByLegend',
filterFunctions: {
...legendDataFilterFunctions.current,
},
},
});
// 我使用TreeGraph进行layout布局采用{type: 'compactBox',direction: 'LR'}模式,如何使子节点与根节点的连线只连接到上下连接桩上
graphRef.current = new G6.TreeGraph({
container: 'semanticGraph',
width,
height,
linkCenter: true,
modes: {
default: [
{
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: {
size: 26,
labelCfg: {
position: 'right',
offset: 5,
style: {
stroke: '#fff',
lineWidth: 4,
},
},
},
defaultEdge: {
type: 'cubic-horizontal',
// type: 'flow-line',
// type: 'polyline',
// type: 'line',
/* configure the bending radius and min distance to the end nodes */
style: {
radius: 10,
offset: 30,
endArrow: true,
/* and other styles */
// stroke: '#F6BD16',
},
// style: {
// stroke: '#A3B1BF',
// },
},
layout: {
type: 'mindmap',
direction: 'H',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
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;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
legend._cfgs.legendCanvas._events.click = [];
legendCanvas.on('click', (e) => {
const shape = e.target;
const shapeGroup = shape.get('parent');
const shapeGroupId = shapeGroup?.cfg?.id;
if (shapeGroupId) {
const isActive = shapeGroup.get('active');
const targetNode = graph.findById(shapeGroupId);
// const model = targetNode.getModel();
toggleNodeVisibility(graph, targetNode, isActive);
toggleChildrenVisibility(graph, targetNode, isActive);
}
});
const graph = graphRef.current;
graph.node(function (node) {
return {
label: node.name,
labelCfg: { style: { fill: '#3c3c3c' } },
};
});
// graph.data(graphData);
graph.changeData(graphData);
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]);
return (
<>
<Row>
<Col flex="auto" />
<Col flex="100px">
<Radio.Group
buttonStyle="solid"
size="small"
value={graphShowType}
onChange={(e) => {
const { value } = e.target;
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 }) => ({
domainManger,
}))(DomainManger);