This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
cyber-narrator-cn-ui/src/views/entityExplorer/EntityGraph.vue

1149 lines
42 KiB
Vue
Raw Normal View History

2023-06-16 17:18:58 +08:00
<template>
<div class="entity-graph">
2023-06-29 14:40:50 +08:00
<div class="entity-graph__chart" id="entityGraph">
</div>
2023-07-02 22:38:59 +08:00
<div class="entity-graph__right-box">
<el-drawer
v-model="rightBox.show"
direction="rtl"
custom-class="entity-graph__detail"
class="entity-graph__detail"
2023-07-09 21:51:05 +08:00
:close-on-click-modal="true"
2023-07-02 22:38:59 +08:00
:modal="false"
2023-07-09 21:51:05 +08:00
:size="400"
2023-07-02 22:38:59 +08:00
:with-header="false"
destroy-on-close>
<graph-entity-list
v-if="rightBox.mode === 'list'"
:node="rightBox.node"
:loading="rightBox.loading"
2023-07-09 21:51:05 +08:00
@expandList="expandList"
2023-07-02 22:38:59 +08:00
@closeBlock="onCloseBlock"
>
</graph-entity-list>
<graph-entity-detail
v-else-if="rightBox.mode === 'detail'"
:node="rightBox.node"
:loading="rightBox.loading"
2023-07-09 21:51:05 +08:00
@expandDetailList="expandDetailList"
@closeBlock="onCloseBlock"
>
</graph-entity-detail>
2023-07-02 22:38:59 +08:00
</el-drawer>
2023-06-16 17:18:58 +08:00
</div>
</div>
</template>
<script>
import GraphEntityList from '@/views/entityExplorer/entityGraph/GraphEntityList'
import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDetail'
import { useRoute } from 'vue-router'
2023-06-29 14:40:50 +08:00
import { ref, shallowRef } from 'vue'
import _ from 'lodash'
2023-07-12 17:23:42 +08:00
import G6, { Algorithm } from '@antv/g6'
import Node, { nodeType, queryRelatedEntity } from './entityGraph/node'
import Edge from './entityGraph/edge'
2023-06-16 17:18:58 +08:00
export default {
name: 'EntityRelationship',
components: {
GraphEntityList,
GraphEntityDetail
2023-06-16 17:18:58 +08:00
},
data () {
return {
debounceFunc: null,
2023-06-30 18:43:02 +08:00
chartOption: {
2023-07-02 22:38:59 +08:00
container: 'entityGraph',
layout: {
type: 'force',
preventOverlap: true,
linkDistance: (d) => {
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
2023-07-09 21:51:05 +08:00
return 200
2023-07-02 22:38:59 +08:00
}
return 80
},
nodeStrength: (d) => {
if (d.type === nodeType.rootNode || d.type === nodeType.listNode) {
2023-07-09 21:51:05 +08:00
return -100
} else if (d.type === nodeType.tempNode) {
return -300
2023-07-02 22:38:59 +08:00
}
return -10
},
edgeStrength: (d) => {
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
2023-07-02 22:38:59 +08:00
return 0.1
}
return 0.7
}
},
modes: {
2023-07-09 21:51:05 +08:00
default: ['drag-canvas', 'drag-nodes', 'click-select', 'zoom-canvas']
2023-07-02 22:38:59 +08:00
}
},
/* 自己实现stack操作 */
stackData: {
undo: [], // 后退
justUndo: false, // 是否刚后退了
redo: [], // 前进
justRedo: false // 是否刚前进了
},
center: {},
initialData: null // 初始化数据,用于重置
}
},
methods: {
2023-06-29 14:40:50 +08:00
async init () {
this.registerElements() // 注册自定义node
2023-07-09 21:51:05 +08:00
const tooltip = this.buildTooltip() // tooltip组件
const toolbar = this.buildToolbar() // 工具栏组装件
this.chartOption.plugins = [tooltip, toolbar] // 注册组件
2023-07-09 21:51:05 +08:00
this.graph = new G6.Graph(this.chartOption)
try {
const initialData = await this.generateInitialData() // 备份初始数据
this.initialData = _.cloneDeep(initialData) // 初始化数据
this.graph.data(initialData)
this.graph.render()
await this.$nextTick(() => {
const { x, y } = this.graph.getViewPortCenterPoint()
this.center = { x, y }
})
const rootNode = this.graph.findById(this.entity.entityName)
this.bindEvents() // 绑定事件
this.graph.emit('node:click', { item: rootNode, target: rootNode.getKeyShape() }) // 手动触发rootNode的点击事件
} catch (e) {
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
}
},
registerElements () {
const _this = this
G6.registerNode(
nodeType.rootNode,
{
draw (cfg, group) {
group.addShape('circle', {
name: 'rootSelectedShadow',
attrs: {
r: 25, // selected时38.5
fill: getRootNodeStyle(cfg.myData.entityType).selectedShadowColor
2023-07-09 21:51:05 +08:00
}
})
group.addShape('circle', {
name: 'rootShadow',
attrs: {
r: 31.5,
fill: getRootNodeStyle(cfg.myData.entityType).shadowColor
}
})
const node = group.addShape('circle', {
name: 'rootInner',
attrs: {
r: 26,
fx: cfg.fx,
fy: cfg.fy,
fill: '#FFFFFF',
stroke: getRootNodeStyle(cfg.myData.entityType).borderColor,
lineWidth: 1
}
})
group.addShape('image', {
attrs: {
x: -getRootNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getRootNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getRootNodeStyle(cfg.myData.entityType).iconHeight,
width: getRootNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, true, true)
},
name: 'rootIcon',
draggable: true
})
// 为label建一个box避免鼠标事件中断
group.addShape('rect', {
name: 'rootLabelBox',
attrs: {
width: 64,
height: 40,
x: -32,
y: 10,
fill: 'transparent'
}
})
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 42,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636'
},
name: 'rootLabel'
})
return node
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const selectedShadowShape = group.get('children')[0]
const shadowShape = group.get('children')[1]
if (name === 'hover') {
// 鼠标悬浮时若不处于selected状态才继续处理
if (!node.hasState('mySelected')) {
// hover时加深shadow的颜色
if (value) {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
} else {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
}
}
} else if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
// 变更样式
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
selectedShadowShape.attr('r', 38.5)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
2023-07-09 21:51:05 +08:00
} else {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
selectedShadowShape.attr('r', 25)
2023-07-09 21:51:05 +08:00
}
}
}
},
'single-node'
)
G6.registerNode(
nodeType.listNode,
{
draw (cfg, group) {
group.addShape('circle', {
name: 'listShadow',
attrs: {
r: 20, // selected时26.5
x: 0,
y: 0,
fill: getListNodeStyle(cfg.myData.entityType).hoveredShadowColor
}
})
const node = group.addShape('circle', {
name: 'listInner',
attrs: {
r: 21,
fx: cfg.fx,
fy: cfg.fy,
fill: '#FFFFFF',
stroke: getListNodeStyle(cfg.myData.entityType).borderColor,
lineWidth: 1
}
})
group.addShape('image', {
attrs: {
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, false, false)
},
name: 'listIcon',
draggable: true
})
// 为label建一个box避免鼠标事件中断
group.addShape('rect', {
name: 'rootLabelBox',
attrs: {
width: 46,
height: 30,
x: -23,
y: 12,
fill: 'transparent'
}
})
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 34,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636'
},
name: 'listLabel'
})
return node
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const shadowShape = group.get('children')[0]
const innerShape = group.get('children')[1]
if (name === 'hover') {
// 鼠标悬浮时若不处于selected状态才继续处理
if (!node.hasState('mySelected')) {
// hover时出现shadow
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
if (value) {
shadowShape.attr('r', 26.5)
} else {
shadowShape.attr('r', 20)
2023-07-09 21:51:05 +08:00
}
}
} else if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
2023-07-09 21:51:05 +08:00
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).selectedShadowColor)
shadowShape.attr('r', 26.5)
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).selectedBorderColor)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
} else {
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
shadowShape.attr('r', 20)
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).borderColor)
2023-07-09 21:51:05 +08:00
}
}
}
},
'single-node'
)
G6.registerNode(
nodeType.entityNode,
{
draw (cfg, group) {
group.addShape('circle', {
name: 'entityShadow',
attrs: {
r: 18,
x: 0,
y: 0,
fill: 'transparent'
}
})
return group.addShape('image', {
attrs: {
x: -getEntityNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getEntityNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getEntityNodeStyle(cfg.myData.entityType).iconHeight,
width: getEntityNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, true, false)
},
name: 'entityIcon'
2023-07-09 21:51:05 +08:00
})
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const shadowShape = group.get('children')[0]
if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getEntityNodeStyle(model.myData.entityType).selectedShadowColor)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
} else {
shadowShape.attr('fill', 'transparent')
}
}
2023-07-09 21:51:05 +08:00
}
},
'single-node'
)
G6.registerNode(
nodeType.tempNode,
{
draw (cfg, group) {
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 22,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636',
opacity: 0.7
},
name: 'tempLabel'
})
return group.addShape('image', {
attrs: {
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, false, false),
opacity: 0.7
},
name: 'tempIcon'
})
},
setState (name, value, node) {
// 重写setState避免鼠标事件导致重新加载图片
2023-07-09 21:51:05 +08:00
}
},
'single-node'
)
function getRootNodeStyle (entityType) {
switch (entityType) {
case 'ip': {
return {
iconWidth: 26,
iconHeight: 23,
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
2023-07-09 21:51:05 +08:00
}
}
case 'domain': {
return {
iconWidth: 28,
iconHeight: 24,
borderColor: 'rgba(56,172,210,1)',
shadowColor: 'rgba(56,172,210,0.21)',
hoveredShadowColor: 'rgba(56,172,210,0.36)',
selectedShadowColor: 'rgba(56,172,210,0.1)'
}
2023-07-09 21:51:05 +08:00
}
case 'app': {
return {
iconWidth: 24,
iconHeight: 26,
borderColor: 'rgba(229,162,25,1)',
shadowColor: 'rgba(229,162,25,0.21)',
hoveredShadowColor: 'rgba(229,162,25,0.36)',
selectedShadowColor: 'rgba(229,162,25,0.1)'
2023-07-09 21:51:05 +08:00
}
2023-07-02 22:38:59 +08:00
}
}
return {
iconWidth: 26,
iconHeight: 23,
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
2023-07-02 22:38:59 +08:00
}
2023-06-29 14:40:50 +08:00
}
function getListNodeStyle (entityType) {
switch (entityType) {
case 'ip': {
return {
iconWidth: 24,
iconHeight: 21,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.21)'
}
2023-07-02 22:38:59 +08:00
}
case 'domain': {
return {
iconWidth: 24,
iconHeight: 24,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(56,172,210,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(56,172,210,0.21)'
2023-07-12 17:23:42 +08:00
}
2023-07-14 16:50:30 +08:00
}
case 'app': {
return {
iconWidth: 22,
iconHeight: 24,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(229,162,25,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(229,162,25,0.21)'
}
2023-07-14 16:50:30 +08:00
}
2023-07-09 21:51:05 +08:00
}
return {
iconWidth: 24,
iconHeight: 21,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.21)'
2023-07-09 21:51:05 +08:00
}
}
function getEntityNodeStyle (entityType) {
switch (entityType) {
2023-06-30 18:43:02 +08:00
case 'ip': {
return {
iconWidth: 22,
iconHeight: 20,
selectedShadowColor: 'rgba(126,159,84,0.1)'
2023-06-30 18:43:02 +08:00
}
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
case 'domain': {
return {
iconWidth: 22,
iconHeight: 22,
selectedShadowColor: 'rgba(56,172,210,0.1)'
2023-06-30 18:43:02 +08:00
}
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
case 'app': {
return {
iconWidth: 21,
iconHeight: 24,
selectedShadowColor: 'rgba(229,162,25,0.1)'
2023-06-30 18:43:02 +08:00
}
2023-06-29 14:40:50 +08:00
}
}
return {
iconWidth: 22,
iconHeight: 20,
borderColor: 'rgba(119,131,145,1)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
2023-06-30 18:43:02 +08:00
}
}
function getIconUrl (entityType, colored, isRoot) {
let suffix
if (entityType === 'domain' && isRoot) {
suffix = '-colored2'
} else {
suffix = colored ? '-colored' : ''
}
return require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
2023-07-09 21:51:05 +08:00
}
},
bindEvents () {
const _this = this
this.graph.on('node:dragstart', (e) => {
_this.graph.layout()
refreshDraggedNodePosition(e)
2023-07-09 21:51:05 +08:00
})
this.graph.on('node:drag', function (e) {
refreshDraggedNodePosition(e)
2023-06-29 14:40:50 +08:00
})
this.graph.on('node:dragend', function (e) {
e.item.get('model').fx = null
e.item.get('model').fy = null
2023-06-30 18:43:02 +08:00
})
this.graph.on('node:click', async function (e) {
const node = e.item
const nodeModel = node.get('model')
if (nodeModel.type !== 'tempNode') {
_this.rightBox.show = true
node.setState('mySelected', true)
_this.cleanTempNodesAndTempEdges()
// 点击entityNode查询数据并根据数据生成tempNode
if (nodeModel.type === nodeType.entityNode) {
_this.rightBox.loading = true
try {
// 若已查过数据,不重复查询
if (!nodeModel.myData.relatedEntity) {
await nodeModel.queryDetailData()
}
let change = false
Object.keys(nodeModel.myData.relatedEntity).forEach(k => {
if (nodeModel.myData.relatedEntity[k].total) {
// 若已有同级同类型的listNode不生成此tempNode
const neighbors = _this.graph.getNeighbors(nodeModel.id, 'target')
const hasListNode = neighbors.some(b => b.get('model').myData.entityType === k)
if (!hasListNode) {
change = true
const tempNode = new Node(
nodeType.tempNode,
`${nodeModel.id}__${k}__temp`,
{
entityType: k,
...generateTempNodeCoordinate(nodeModel.sourceNode, e)
},
nodeModel
)
const tempEdge = new Edge(nodeModel, tempNode, 'temp')
_this.graph.addItem('node', tempNode)
_this.graph.addItem('edge', tempEdge)
2023-07-02 22:38:59 +08:00
}
}
2023-06-30 18:43:02 +08:00
})
change && _this.graph.layout()
_this.rightBox.node = _.cloneDeep(nodeModel)
_this.rightBox.mode = 'detail'
} catch (e) {
_this.$message.error(_this.errorMsgHandler(e))
} finally {
_this.rightBox.loading = false
}
} else if (nodeModel.type === nodeType.listNode) {
_this.rightBox.node = _.cloneDeep(nodeModel)
_this.rightBox.mode = 'list'
} else if (nodeModel.type === nodeType.rootNode) {
_this.rightBox.node = _.cloneDeep(nodeModel)
_this.rightBox.mode = 'detail'
2023-06-30 18:43:02 +08:00
}
2023-07-09 21:51:05 +08:00
} else {
// 点击tempNode根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。
// 若已达第六层则只生成listNode不再展开entityNode
const nodes = []
const edges = []
const listNode = new Node(
nodeType.listNode,
`${nodeModel.sourceNode.id}__${nodeModel.myData.entityType}-list`,
{
entityType: nodeModel.myData.entityType,
x: nodeModel.x,
y: nodeModel.y
},
nodeModel.sourceNode
)
nodes.push(listNode)
edges.push(new Edge(nodeModel.sourceNode, listNode))
// 判断listNode的sourceNode层级若大于等于10即第6层listNode则不继续拓展entity node并给用户提示。否则拓展entity node
const level = _this.getNodeLevel(listNode.sourceNode.id)
if (level < 10) {
_this.rightBox.loading = true
try {
const entities = await queryRelatedEntity(nodeModel.sourceNode, listNode.myData.entityType)
nodeModel.sourceNode.myData.relatedEntity[listNode.myData.entityType].list.push(...entities.list)
entities.list.forEach(entity => {
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
entityType: listNode.myData.entityType,
entityName: entity.vertex,
x: e.x + Math.random() * 100 - 50,
y: e.y + Math.random() * 100 - 50
}, listNode)
nodes.push(entityNode)
edges.push(new Edge(listNode, entityNode))
})
} catch (e) {
_this.$message.error(_this.errorMsgHandler(e))
} finally {
_this.rightBox.loading = false
}
2023-07-09 21:51:05 +08:00
} else {
this.$message.warning(this.$t('tip.maxExpandDepth'))
2023-07-09 21:51:05 +08:00
}
_this.addItems(nodes, edges)
_this.cleanTempNodesAndTempEdges()
_this.graph.layout()
// 手动高亮listNode
const _listNode = _this.graph.findById(listNode.id)
_this.graph.emit('node:click', { item: _listNode, target: _listNode.getKeyShape() })
if (_this.stackData.justUndo) {
_this.stackData.justUndo = false
_this.stackData.redo = []
}
if (_this.stackData.justRedo) {
_this.stackData.justRedo = false
_this.stackData.redo = []
}
2023-07-09 21:51:05 +08:00
}
})
this.graph.on('node:mouseenter', function (e) {
e.item.setState('hover', true)
})
this.graph.on('node:mouseleave', function (e) {
e.item.setState('hover', false)
})
function refreshDraggedNodePosition (e) {
const model = e.item.get('model')
model.fx = e.x
model.fy = e.y
2023-07-09 21:51:05 +08:00
}
function generateTempNodeCoordinate (sourceNode, event) {
const sx = sourceNode.x
const sy = sourceNode.y
const cx = event.x
const cy = event.y
return {
x: cx + cx - sx + Math.random() * 30 - 15,
y: cy + cy - sy + Math.random() * 30 - 15
2023-07-09 21:51:05 +08:00
}
}
},
addItems (nodes = [], edges = [], stack = true) {
// 过滤掉已经存在的node
const _nodes = nodes.filter(n => {
return !this.graph.findById(n.id)
})
_nodes.forEach(n => {
this.graph.addItem('node', n)
})
edges.forEach(e => {
this.graph.addItem('edge', e)
})
if (stack) {
this.stackData.undo.push({ nodes: _nodes, edges })
2023-07-09 21:51:05 +08:00
}
},
setItemsStata (models = [], stateName, state) {
models.forEach(m => {
this.graph.setItemState(this.graph.findById(m.id), stateName, state)
2023-07-09 21:51:05 +08:00
})
},
cleanTempNodesAndTempEdges () {
// 清除现有tempNode和tempEdge
const tempNodes = this.graph.findAll('node', node => node.get('model').type === nodeType.tempNode)
tempNodes.forEach(n => {
this.graph.removeItem(n)
})
const tempEdges = this.graph.findAll('edge', edge => edge.get('model').isTemp)
tempEdges.forEach(n => {
this.graph.removeItem(n)
2023-07-09 21:51:05 +08:00
})
},
async generateInitialData () {
const nodes = []
const edges = []
// 生成rootNode并查询相关数据
const { offsetWidth, offsetHeight } = document.getElementById('entityGraph')
const coordinate = {
rootNode: {
fx: offsetWidth * 0.4,
fy: offsetHeight * 0.3 - 20
}
}
coordinate.ipListNode = {
fx: coordinate.rootNode.fx + 200,
fy: coordinate.rootNode.fy
}
coordinate.domainListNode = {
fx: coordinate.rootNode.fx + 100,
fy: coordinate.rootNode.fy + 173
}
coordinate.appListNode = {
fx: coordinate.rootNode.fx - 100,
fy: coordinate.rootNode.fy + 173
}
const rootNode = new Node(nodeType.rootNode, this.entity.entityName, {
entityType: this.entity.entityType,
2023-08-29 15:35:22 +08:00
entityName: this.entity.entityName
/* x: coordinate.rootNode.fx,
y: coordinate.rootNode.fy */
2023-07-09 21:51:05 +08:00
})
await rootNode.queryDetailData()
nodes.push(rootNode)
// 生成listNode和entityNode及edge
if (rootNode.myData.relatedEntity) {
// listNode
const listNodes = []
const keys = Object.keys(rootNode.myData.relatedEntity)
keys.forEach(k => {
if (rootNode.myData.relatedEntity[k].total) {
const listNode = new Node(
nodeType.listNode,
`${rootNode.id}__${k}-list`,
{
2023-08-29 15:35:22 +08:00
entityType: k
/* x: coordinate[`${k}ListNode`].fx,
y: coordinate[`${k}ListNode`].fy */
},
rootNode
)
listNodes.push(listNode)
edges.push(new Edge(rootNode, listNode))
}
})
// entityNode
const entityNodes = []
for (const node of listNodes) {
const entities = await queryRelatedEntity(rootNode, node.myData.entityType)
rootNode.myData.relatedEntity[node.myData.entityType].list = entities.list
entities.list.forEach(entity => {
const entityNode = new Node(
nodeType.entityNode,
entity.vertex,
{
entityType: node.myData.entityType,
entityName: entity.vertex
},
node
)
entityNodes.push(entityNode)
edges.push(new Edge(node, entityNode))
})
}
nodes.push(...listNodes, ...entityNodes)
}
return {
nodes: nodes,
edges: edges
}
2023-07-09 21:51:05 +08:00
},
getNodeLevel (id) {
2023-07-12 17:23:42 +08:00
const { findShortestPath } = Algorithm
const info = findShortestPath(this.graph.save(), this.entity.entityName, id)
2023-07-12 17:23:42 +08:00
return info.length
2023-07-09 21:51:05 +08:00
},
onCloseBlock () {
2023-07-02 22:38:59 +08:00
this.rightBox.mode = ''
this.rightBox.show = false
},
2023-07-09 21:51:05 +08:00
buildTooltip () {
const _this = this
return new G6.Tooltip({
offsetX: 10,
offsetY: 20,
itemTypes: ['node'],
getContent (e) {
const node = e.item.getModel()
if (node.type === nodeType.listNode) {
2023-07-09 21:51:05 +08:00
let iconClass = ''
let title = ''
let total = 0
let loadedCount = 0
switch (node.myData.entityType) {
2023-07-09 21:51:05 +08:00
case 'ip': {
iconClass = 'cn-icon cn-icon-resolve-ip'
title = _this.$t('entities.graph.resolveIp')
total = _.get(node.sourceNode.myData, 'relatedEntity.ip.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.ip.list', []).length
2023-07-09 21:51:05 +08:00
break
}
case 'domain': {
iconClass = 'cn-icon cn-icon-subdomain'
title = node.isSubdomain() ? _this.$t('entities.subdomain') : _this.$t('entity.graph.resolveDomain')
total = _.get(node.sourceNode.myData, 'relatedEntity.domain.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.domain.list', []).length
2023-07-09 21:51:05 +08:00
break
}
case 'app': {
iconClass = 'cn-icon cn-icon-app-name'
title = _this.$t('entities.tab.relatedApp')
total = _.get(node.sourceNode.myData, 'relatedEntity.app.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.app.list', []).length
2023-07-09 21:51:05 +08:00
break
}
}
return `<div class="primary-node-tooltip">
<div class="tooltip__header"><i class="${iconClass}"></i><span class="tooltip__title">${title}</span></div>
<div class="tooltip__content">
<span>${_this.$t('entity.graph.associatedCount')}:&nbsp;${total}</span>
<span>${_this.$t('entity.graph.expandedEntityCount')}:&nbsp;${loadedCount}</span>
2023-07-09 21:51:05 +08:00
</div>
</div>`
} else if (node.type === nodeType.entityNode || node.type === nodeType.rootNode) {
if (node.myData && node.myData.tags && node.myData.tags.length > 0) {
2023-07-12 17:23:42 +08:00
return `<div class="entity-node-tooltip">
<div class="tooltip__header">
<span class="tooltip__title">${node.id}</span>
</div>
<div class="tooltip__content">
<div class="content-header">
<div class="header-icon"></div>
<span>${_this.$t('entity.graph.tags')}</span>
</div>
<div class="content-tag-list">${generateTagHTML(node.myData.tags)}</div>
2023-07-12 17:23:42 +08:00
</div>
</div>`
} else {
return `<div style="padding: 0 6px; font-size: 15px; line-height: 15px; color: #111;">${node.id}</div>`
}
} else if (node.type === nodeType.tempNode) {
2023-07-12 17:23:42 +08:00
return node.label
}
function generateTagHTML (tags) {
let html = ''
if (tags) {
tags.forEach(t => {
html += `<div class="entity-tag entity-tag--level-two-${t.type}">
<span>${t.value}</span>
</div>`
})
}
return html
2023-07-09 21:51:05 +08:00
}
}
})
},
2023-07-14 16:50:30 +08:00
buildToolbar () {
const tc = document.createElement('div')
tc.id = 'toolbarContainer'
tc.className = 'graph-toolbar'
document.body.appendChild(tc)
const toolbar = new G6.ToolBar({
container: tc,
className: 'toolbar__tools',
getContent: () => {
return `<ul>
<li code='zoomOut' title="${this.$t('overall.zoomOut')}"><i class="cn-icon cn-icon-zoom-out"></i></li>
<li code='zoomIn' title="${this.$t('overall.zoomIn')}"><i class="cn-icon cn-icon-zoom-in"></i></li>
<li code='autoZoom' title="${this.$t('overall.autoZoom')}"><i class="cn-icon cn-icon-reset"></i></li>
<li code='undo' title="${this.$t('overall.preStep')}" id="preStep" class="toolbar--unactivated"><i class="cn-icon cn-icon-pre-step"></i></li>
<li code='redo' title="${this.$t('overall.nextStep')}" id="nextStep" class="toolbar--unactivated"><i class="cn-icon cn-icon-next-step"></i></li>
<li code='toDefault' title="${this.$t('overall.reset')}"><i class="cn-icon cn-icon-to-default"></i></li>
2023-07-14 16:50:30 +08:00
</ul>`
},
handleClick: (code, graph) => {
if (code === 'undo') { // 删掉刚加进来的node和edge
const data = this.stackData.undo.pop()
this.stackData.justUndo = true
data.nodes.forEach(n => {
if (n.type === nodeType.listNode) {
const listNode = this.graph.findById(n.id)
const listNodeEdges = listNode.getEdges()
listNodeEdges.forEach(e => {
e.setState('mySelected', false)
})
listNode.setState('mySelected', false)
}
this.graph.removeItem(n.id)
})
data.edges.forEach(e => {
this.graph.removeItem(e.id)
})
this.stackData.redo.push(data)
if (this.stackData.justRedo) {
this.stackData.justRedo = false
}
this.cleanTempNodesAndTempEdges()
this.graph.layout()
this.onCloseBlock()
// graph中删掉后右侧列表也得删
} else if (code === 'redo') { // 恢复刚删掉的node和edge
const data = this.stackData.redo.pop()
this.stackData.justRedo = true
this.addItems(data.nodes, data.edges)
if (this.stackData.justUndo) {
this.stackData.justUndo = false
}
this.cleanTempNodesAndTempEdges()
this.graph.layout()
this.onCloseBlock()
// graph中恢复后右侧列表也得恢复
} else if (code === 'autoZoom') {
this.graph.zoomTo(1)
this.graph.fitCenter()
} else if (code === 'zoomOut') {
this.graph.zoomTo(this.graph.getZoom() + 0.2, this.center)
} else if (code === 'zoomIn') {
const currentZoom = this.graph.getZoom()
this.graph.zoomTo(currentZoom - 0.2, this.center)
} else {
this.graph.clear()
this.graph.data(this.initialData)
this.graph.render()
this.stackData = {
undo: [],
redo: [],
justUndo: false,
justRedo: false
}
const rootNode = this.graph.findById(this.entity.entityName)
this.graph.emit('node:click', { item: rootNode, target: rootNode.getKeyShape() }) // 手动触发rootNode的点击事件
}
2023-07-14 16:50:30 +08:00
}
})
return toolbar
},
async expandList (nodeId) {
const node = this.graph.findById(nodeId)
const model = node.getModel()
const sourceNode = this.graph.findById(model.sourceNode.id)
const sourceModel = sourceNode.getModel()
const expandType = model.myData.entityType
if (sourceModel.myData.relatedEntity[expandType].list.length >= sourceModel.myData.relatedEntity[expandType].total) {
return
}
if (sourceModel.myData.relatedEntity[expandType].list.length < 50) {
this.rightBox.loading = true
try {
const entities = await queryRelatedEntity(sourceModel, expandType)
sourceModel.myData.relatedEntity[expandType].list.push(...entities.list)
const entityNodeModels = []
const edgeModels = []
entities.list.forEach(entity => {
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
entityType: expandType,
entityName: entity.vertex,
x: model.x + Math.random() * 500 - 250,
y: model.y + Math.random() * 500 - 250
}, model)
entityNodeModels.push(entityNodeModel)
2023-07-12 17:23:42 +08:00
const edge = new Edge(model, entityNodeModel)
edgeModels.push(edge)
})
this.addItems(entityNodeModels, edgeModels)
this.setItemsStata(edgeModels, 'mySelected', true)
this.graph.layout()
this.rightBox.node = _.cloneDeep(model)
} catch (e) {
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
2023-07-09 21:51:05 +08:00
}
} else {
this.$message.warning(this.$t('tip.maxExpandCount'))
2023-07-09 21:51:05 +08:00
}
},
async expandDetailList (nodeId, expandType) {
const node = this.graph.findById(nodeId)
if (node) {
const nodeModel = node.getModel()
if (nodeModel.myData.relatedEntity[expandType].list.length >= nodeModel.myData.relatedEntity[expandType].total) {
return
}
if (nodeModel.myData.relatedEntity[expandType].list.length < 50) {
const toAddNodeModels = []
const toAddEdgeModels = []
this.rightBox.loading = true
try {
const entities = await queryRelatedEntity(nodeModel, expandType)
nodeModel.myData.relatedEntity[expandType].list.push(...entities.list)
const neighbors = node.getNeighbors('target')
const listNode = neighbors.find(n => n.getModel().myData.entityType === expandType)
let listNodeModel = listNode.getModel()
let listEdgeModel
// 如果listNode是tempNode移除并新建listNode
if (listNodeModel.type === nodeType.tempNode) {
// 移除tempNode和tempEdge
const tempEdges = listNode.getInEdges()
tempEdges.forEach(edge => {
this.graph.removeItem(edge)
})
this.graph.removeItem(listNode)
listNodeModel = new Node(nodeType.listNode, `${nodeModel.id}__${expandType}-list`, { entityType: expandType, x: listNodeModel.x, y: listNodeModel.y }, nodeModel)
listEdgeModel = new Edge(nodeModel, listNodeModel)
toAddNodeModels.push(listNodeModel)
toAddEdgeModels.push(listEdgeModel)
2023-07-09 21:51:05 +08:00
}
entities.list.forEach(entity => {
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
entityType: expandType,
entityName: entity.vertex,
x: listNodeModel.x + Math.random() * 500 - 250,
y: listNodeModel.y + Math.random() * 500 - 250
}, listNodeModel)
toAddNodeModels.push(entityNodeModel)
toAddEdgeModels.push(new Edge(listNodeModel, entityNodeModel))
})
this.addItems(toAddNodeModels, toAddEdgeModels)
if (listEdgeModel) {
this.graph.setItemState(this.graph.findById(listEdgeModel.id), 'mySelected', true)
}
this.graph.layout()
this.rightBox.node = _.cloneDeep(nodeModel)
} catch (e) {
this.$message.error(this.errorMsgHandler(e))
} finally {
this.rightBox.loading = false
2023-07-09 21:51:05 +08:00
}
} else {
this.$message.warning(this.$t('tip.maxExpandCount'))
2023-07-09 21:51:05 +08:00
}
}
},
resize () {
const container = document.getElementById('entityGraph')
this.graph.changeSize(container.offsetWidth, container.offsetHeight)
}
},
watch: {
stackData: {
deep: true,
handler (n) {
if (n) {
if (n.undo.length > 0) {
document.getElementById('preStep').classList.remove('toolbar--unactivated')
} else {
document.getElementById('preStep').classList.add('toolbar--unactivated')
}
if (n.redo.length > 0) {
document.getElementById('nextStep').classList.remove('toolbar--unactivated')
} else {
document.getElementById('nextStep').classList.add('toolbar--unactivated')
}
}
2023-07-09 21:51:05 +08:00
}
2023-06-16 17:18:58 +08:00
}
2023-06-29 14:40:50 +08:00
},
async mounted () {
if (this.entity.entityType && this.entity.entityName) {
await this.init()
}
this.debounceFunc = this.$_.debounce(this.resize, 300)
window.addEventListener('resize', this.debounceFunc)
},
unmounted () {
window.removeEventListener('resize', this.debounceFunc)
this.graph.destroy()
2023-06-29 14:40:50 +08:00
},
setup () {
const route = useRoute()
const { entityType, entityName } = route.query
const entity = {
2023-06-29 14:40:50 +08:00
entityType,
entityName
}
// 关系图
2023-07-09 21:51:05 +08:00
const graph = shallowRef(null)
2023-07-02 22:38:59 +08:00
const rightBox = ref({
mode: 'detail', // list | detail
show: true,
node: null,
loading: true
2023-07-02 22:38:59 +08:00
})
2023-06-29 14:40:50 +08:00
return {
entity,
2023-07-09 21:51:05 +08:00
rightBox,
graph
2023-06-29 14:40:50 +08:00
}
2023-06-16 17:18:58 +08:00
}
}
</script>