1 背景
为例描述各个服务、redis、mysql等之间的联系及其健康状态,构建系统拓扑图,考虑 g6 更适合处理大量数据之间的关系,所以我们采用g6来绘制前端的图形。
g6提供的支持:
- 节点/边类型多样,同样支持自定义
- 对于节点的样式可以直接配置化处理
- 丰富的事件体系,包括对节点/边/画布,以及时机事件的监听
- 多种布局算法
- 节点/边的数据,都是可以配置化的json对象
在线工具:g6示例
2 功能列表
节点:
- 添加节点:除了id、style、type外,还包括一些业务需要的数据
- 删除节点:除了删除该节点相对于画布的id外,还包括与之相关的业务数据
- 节点状态:比如错误节点需要标红;非活跃节点需要标灰
边:
- 添加边:除了id、style、type外,还包括一些业务需要的数据
- 删除变:除了删除该边相对于画布的id外,还包括与之相关的业务数据
- 修改边:主要是修改边所代表的业务信息,如果没有业务信息的话,这条边应该被删除
画布:
- 用户自定义布局,比如需要保存用户拖拽节点后的节点位置坐标信息
- dagre层次布局
- 工具栏
- 图例
- 小地图
- 触摸板放大缩小
- 节点搜索
3 节点
3.1 渲染节点
渲染节点,包括自定义节点类型和样式。
自定义节点,该节点由rect和image组成,类似于矩形里面有icon:
// 其实可以不用自定义节点,可以使用circle类型的icon字段。但是这种方式,点击节点的时候,里面的icon会存在闪缩的情况
// https://g6.antv.antgroup.com/manual/middle/elements/nodes/built-in/circle#%E5%9B%BE%E6%A0%87-icon
G6.registerNode(
'drag-inner-image-node',
{
afterDraw(cfg, group) {
const size = cfg?.size as number[];
const width = size[0] - 20;
const height = size[1] - 20;
const imageShape = group?.addShape('image', {
attrs: {
x: -width / 2,
y: -height / 2,
width,
height,
img: cfg?.img,
cursor: 'move',
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: 'image-shape',
});
// 启用拖拽
imageShape?.set('draggable', true);
},
},
'circle',
);
节点样式:
const DefaultNodeSelectedStyle = {
lineWidth: 8,
'text-shape': {
// 点击后的文本样式,保持点击前一致
fontWeight: 400,
},
};
export const NodeStyleMap = {
default: {
// 正常节点 - 样式设置
style: {
fill: GlobalLightBlueColor,
stroke: GlobalBlueColor,
lineWidth: 1,
},
// 状态样式,比如 selected点击状态
stateStyles: {
selected: {
stroke: GlobalBlueColor,
fill: GlobalLightBlueColor,
shadowColor: GlobalBlueColor,
...DefaultNodeSelectedStyle,
},
},
},
error: {
// 异常节点
style: {
stroke: GlobalRedColor,
fill: GlobalLightRedColor,
lineWidth: 1,
},
stateStyles: {
selected: {
stroke: GlobalRedColor,
fill: GlobalLightRedColor,
shadowColor: GlobalRedColor,
...DefaultNodeSelectedStyle,
},
},
},
};
获取节点的渲染数据:
export const formatNodes = (nodes: MttkArchitectureNode[] = []) => {
return nodes?.map((node) => {
const { component, has_error, coordinates } = node;
// 业务逻辑
const middlewareType = getMiddlewareType(component) as MttkComponentType;
const { id, label, wholeLabelName } = getNodeId(node);
// 样式和icon
const nodeStyle = NodeStyleMap[has_error ? 'error' : 'default'];
const img = has_error ? ErrorIconImageMap[middlewareType] : IconImageMap[middlewareType];
return {
...node,
img,
middlewareType,
label,
wholeLabelName, // 仅前端展示使用
...nodeStyle,
id, // 仅前端展示使用
x: coordinates?.x, // 节点的位置坐标
y: coordinates?.y, // 节点的位置坐标
};
});
};
3.2 删除节点
3.3 添加节点
4 边
4.1 渲染边
边的样式:
const DefaultEdgeSelectedStyle = {
lineWidth: 4,
shadowBlur: 10, // 阴影的模糊级别,数值越大越模糊
};
export const EdgeStyleMap = {
default: {
// 正常边 - 样式设置
style: {
stroke: GlobalBlueColor,
lineWidth: 1,
lineDash: [0], // 如果[0]表示直线,需要覆盖一下创建边之后的虚线样式
},
// 状态样式,比如 selected点击状态
stateStyles: {
selected: {
stroke: GlobalBlueColor,
shadowColor: GlobalBlueColor,
...DefaultEdgeSelectedStyle,
},
},
},
error: {
// 异常边
style: {
stroke: GlobalRedColor,
lineWidth: 1,
},
stateStyles: {
selected: {
stroke: GlobalRedColor,
shadowColor: GlobalRedColor,
...DefaultEdgeSelectedStyle,
},
},
},
};
边的渲染数据:
export const formatEdges = (edges: MttkArchitectureEdge[] = [], nodes: MttkArchitectureNode[] = []) => {
return edges?.map((edge) => {
const { has_error } = edge;
const edgeStyle = EdgeStyleMap[has_error ? 'error' : 'default'];
const { id, fromId, toId } = getEdgeId(nodes, edge);
return {
...edge,
source: fromId,
target: toId,
...edgeStyle,
id,
from: fromId, // 前端直接替换掉get接口返回的随机数id
to: toId, // 前端直接替换掉get接口返回的随机数id
};
});
};
4.2 删除边
4.3 添加边
5 画布全局配置
export const LayoutMap = {
[LayoutType.LR]: {
// 从左到右
type: 'dagre',
ranksep: 70,
controlPoints: true, // 是否保留布局连线的控制点
rankdir: 'LR', // 可选,默认为图的中心
nodesep: 10, // 可选
},
[LayoutType.TB]: {
// 从上到下
// type: 'dagre',
// ranksep: 70,
// controlPoints: true,
rankdir: 'TB',
},
};
export const DefaultOptions = {
layout: LayoutMap.LR,
defaultNode: {
type: 'drag-inner-image-node',
size: [50, 50],
style: { cursor: 'move' },
label: 'node-label',
labelCfg: {
position: 'bottom',
offset: 2,
style: {
fill: '#666',
fontSize: 14,
cursor: 'move',
},
},
},
defaultEdge: {
type: 'polyline',
style: {
radius: 20, // 拐弯处的圆角弧度
offset: 20, // 拐弯处距离节点最小距离
endArrow: true,
lineAppendWidth: 20, // 提升边的击中范围
},
},
modes: {
default: [
'drag-canvas',
'drag-node',
{
type: 'create-edge',
trigger: 'click', // 'click' by default. options: 'drag', 'click'
key: 'shift', // undefined by default, options: 'shift', 'control', 'ctrl', 'meta', 'alt'
edgeConfig: {
// 有该交互创建出的边的配置项,可以配置边的类型、样式等
style: {
radius: 20, // 拐弯处的圆角弧度
offset: 20, // 拐弯处距离节点最小距离
endArrow: true,
lineAppendWidth: 20, // 提升边的击中范围
...EdgeStyleMap.default.style,
lineDash: [5], // 设置线的虚线样式, 如果[0]表示直线
},
},
shouldEnd: (e: any, self: any) => {
const { item: toItem } = e;
const { source: fromId, graph } = self;
const toId = toItem._cfg.id;
// 不允许创建自环边
if (toId === fromId) {
return false;
}
// 不允许创建已经存在的边
const edges = graph.getEdges();
if (
edges.some((ed: any) => {
const { source, target } = ed.getModel();
return fromId === source && toId === target;
})
) {
return false;
}
return true;
},
},
{
type: 'click-select',
// 不允许节点被该交互选中。如果为true的话,会存在重复点击当前节点闪烁的情况,
// 因为 已选中 > 再次点击,会默认给当前节点 selected status设置为false,我们再手动改为true的时候,就会存在闪烁
selectNode: false,
multiple: false, // 不允许多选
},
],
},
fitView: true, // 图是否自适应画布
};
6 图例
g6自带的图例不是很好自定义ui,虽然可以进行与节点/边数据联动的功能,所以考虑直接react实现。
// interface Props {
// extendLegend?: React.ReactNode; // 扩展图例,比如错误的信息
// }
export const GraphNodeTypeConfigs = [
{
icon: IconImageMap[MttkComponentType.SERVICE],
description: 'Service',
key: MttkComponentType.SERVICE,
},
{
icon: IconImageMap[MttkComponentType.MYSQL],
description: 'MySQL',
key: MttkComponentType.MYSQL,
},
{
icon: IconImageMap[MttkComponentType.KAFKA],
description: 'Kafka',
key: MttkComponentType.KAFKA,
},
{
icon: IconImageMap[MttkComponentType.REDIS],
description: 'Redis',
key: MttkComponentType.REDIS,
},
{
icon: IconImageMap[MttkComponentType.UNKNOWN],
description: 'Unknown',
key: MttkComponentType.UNKNOWN,
},
];
export function LegendRow() {
return (
<>
{GraphNodeTypeConfigs.map(({ icon, description }) => (
<Row justify="start" align="middle" wrap={false} style={{ marginRight: 8 }}>
<img src={icon} style={{ width: 18, height: 18, marginRight: 4 }} />
{description}
</Row>
))}
</>
);
}
7 工具栏
跟图例一样,考虑不太好自定义ui,所以直接react实现。
import { ZoomInOutlined, ZoomOutOutlined, FullscreenExitOutlined } from '@ant-design/icons';
import { Col, Row, Button } from 'antd';
interface Props {
onZoomIn: () => void; // 放大
onZoomOut: () => void; // 缩小
onFixCenter: () => void; // 回到中间
}
export function Toolbar(props: Props) {
const { onZoomIn, onZoomOut, onFixCenter } = props;
return (
<Col style={{ width: 30 }}>
<Row justify="center">
<Button type="link" style={{ padding: 0 }} onClick={onZoomIn}>
<ZoomInOutlined />
</Button>
</Row>
<Row justify="center">
<Button type="link" style={{ padding: 0 }} onClick={onZoomOut}>
<ZoomOutOutlined />
</Button>
</Row>
<Row justify="center">
<Button type="link" style={{ padding: 0 }} onClick={onFixCenter}>
<FullscreenExitOutlined />
</Button>
</Row>
</Col>
);
}