CN-1087 feat: 关系图基本实现

This commit is contained in:
chenjinsong
2023-06-29 14:40:50 +08:00
parent 4c38f1c913
commit 30883802cb
7 changed files with 282 additions and 43 deletions

View File

@@ -27,6 +27,7 @@
"node-sass": "^4.14.1",
"postcss-plugin-px2rem": "^0.8.1",
"postcss-px2rem-exclude": "0.0.6",
"relation-graph": "^2.0.26",
"sass-loader": "^8.0.2",
"sass-resources-loader": "^2.2.1",
"tiny-emitter": "^2.1.0",
@@ -40,7 +41,9 @@
"@babel/cli": "^7.12.1",
"@babel/core": "^7.11.4",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-private-methods": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/plugin-proposal-private-property-in-object": "^7.12.1",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@commitlint/cli": "^9.1.2",

View File

@@ -252,7 +252,11 @@ export const api = {
openPortOfApp: apiVersion + '/entity/detail/app/relate/ports',
basicInfo: apiVersion + '/entity/detail/basic',
tags: apiVersion + '/entity/detail/kb/intelligence/tag',
informationAggregation: apiVersion + '/entity/detail/kb/intelligence/list'
informationAggregation: apiVersion + '/entity/detail/kb/intelligence/list',
// 实体关系
entityGraph: {
relatedEntityCount: apiVersion + '/entity/graph/relation/summaryCount'
}
}
}

View File

@@ -76,7 +76,7 @@ export default {
setup () {
const { query } = useRoute()
const entityType = query.entityType
const entityName = query.name || query.entityName
const entityName = query.entityName
return {
entityType,

View File

@@ -101,7 +101,7 @@ export default {
setup () {
const { query } = useRoute()
const entityType = query.entityType
const entityName = query.name
const entityName = query.entityName
return {
entityType,

View File

@@ -22,21 +22,21 @@ export default {
setup (props) {
const { query } = useRoute()
let panelType
const entityData = { entityType: query.entityType, entityName: query.name }
const entityData = { entityType: query.entityType, entityName: query.entityName }
switch (query.entityType) {
case 'ip': {
panelType = panelTypeAndRouteMapping.ipEntityDetail
entityData.ip = query.name
entityData.ip = query.entityName
break
}
case 'domain': {
panelType = panelTypeAndRouteMapping.domainEntityDetail
entityData.domain = query.name
entityData.domain = query.entityName
break
}
case 'app': {
panelType = panelTypeAndRouteMapping.appEntityDetail
entityData.appName = query.name
entityData.appName = query.entityName
break
}
default: {

View File

@@ -1,7 +1,23 @@
<template>
<div class="entity-graph">
<div class="entity-graph__chart"></div>
<div class="entity-graph__detail" v-if="mode !== ''">
<div class="entity-graph__chart" id="entityGraph">
<relation-graph ref="relationGraph" :options="chartOption">
<template #node="{ node }">
<!-- root节点 -->
<template v-if="node.data.level === '1'">
<i :class="node.data.iconClass"></i>
<div class="graph-node__text">{{node.text}}</div>
</template>
<!-- primary节点 -->
<template v-else-if="node.data.level && node.data.level.length === 1">
<i :class="node.data.iconClass"></i>
<div class="graph-node__text">{{node.text}}</div>
</template>
<!-- entity节点 -->
</template>
</relation-graph>
</div>
<div class="entity-graph__detail">
<ip-list
v-if="mode === 'ipList'"
:entity="entity"
@@ -31,51 +47,167 @@ import IpList from '@/views/entityExplorer/entityGraphDetail/IpList'
import GraphDetail from '@/views/entityExplorer/entityGraphDetail/GraphDetail'
import AppOrDomainList from '@/views/entityExplorer/entityGraphDetail/AppOrDomainList'
import { useRoute } from 'vue-router'
import { ref } from 'vue'
import { ref, shallowRef } from 'vue'
import _ from 'lodash'
import { api } from '@/utils/api'
import axios from 'axios'
import RelationGraph from 'relation-graph/vue3'
export default {
name: 'EntityRelationship',
components: {
IpList,
GraphDetail,
AppOrDomainList
},
setup () {
const route = useRoute()
const { entityType, name } = route.query
const entity = ref({
entityType: entityType,
entityName: name
})
const mode = ref('')
switch (entityType) {
case 'ip':
{
mode.value = 'ipList'
break
}
case 'domain':
{
mode.value = 'domainList'
break
}
case 'app':
{
mode.value = 'appList'
}
}
return {
entity,
mode
}
AppOrDomainList,
RelationGraph
},
data () {
return {
// mode: 'appList' // ipList, ipDetail, domainList, domainDetail, appList, appDetail
chartOption: {}
}
},
methods: {
async init () {
/* 定义了以下几种节点类型和节点状态:
* 1.类型:根节点 root状态ip、app、domain;
* 2.类型:普通节点 primary状态normal、active;
* 3.类型:实体 entity状态1ip、app、domain状态2normal、active.
* ip基色#7E9F54domain基色#38ACD2app基色E5A219
* */
this.graphData.rootId = this.entity.entityName
const rootNode = this.generateRootNode()
const secondLevelNodes = await this.generateSecondLevelNodes()
this.graphData.nodes = [rootNode, ...secondLevelNodes]
this.graphData.lines = this.generateLines(rootNode, secondLevelNodes)
console.info(this.graphData)
this.$refs.relationGraph.setJsonData(this.graphData)
},
generateRootNode () {
let nodeIconClass = ''
switch (this.entity.entityType) {
case 'ip': {
nodeIconClass = 'cn-icon cn-icon-resolve-ip'
break
}
case 'domain': {
nodeIconClass = 'cn-icon cn-icon-domain1'
break
}
case 'app': {
nodeIconClass = 'cn-icon cn-icon-app-name'
break
}
}
return {
id: this.entity.entityName,
text: this.entity.entityName,
nodeShape: 0,
styleClass: `graph-node graph-node--root graph-node--${this.entity.entityType}`,
data: { level: '1', iconClass: nodeIconClass }
}
},
generatePrimaryNode (props) {
const node = {
styleClass: `graph-node graph-node--primary graph-node--${this.entity.entityType}`
}
return {
...node,
...props
}
},
async generateSecondLevelNodes () {
/*
* 2级及以上的整数节点先查数量大于0的才展示
* */
const relatedEntityCount = await this.queryRelatedEntityCount(this.entity.entityType, this.entity.entityName)
const ipNode = this.generatePrimaryNode({
id: 'ip-1',
text: this.$t('entities.graph.resolveIp'),
data: { level: '2', iconClass: 'cn-icon cn-icon-resolve-ip' }
})
const domainNode = this.generatePrimaryNode({
id: 'domain-1',
text: this.$t('entity.graph.resolveDomain'),
data: { level: '2', iconClass: 'cn-icon cn-icon-subdomain' }
})
const subdomainNode = this.generatePrimaryNode({
id: 'domain-1',
text: this.$t('entities.subdomain'),
data: { level: '2', iconClass: 'cn-icon cn-icon-subdomain' }
})
const appNode = this.generatePrimaryNode({
id: 'app-1',
text: this.$t('entities.tab.relatedApp'),
data: { level: '2', iconClass: 'cn-icon cn-icon-app-name' }
})
const nodes = []
switch (this.entity.entityType) {
case 'ip': {
if (relatedEntityCount.domainCount) {
domainNode.data.count = relatedEntityCount.domainCount
nodes.push(domainNode)
}
if (relatedEntityCount.appCount) {
domainNode.data.count = relatedEntityCount.appCount
nodes.push(appNode)
}
break
}
case 'domain': {
if (relatedEntityCount.ipCount) {
ipNode.data.count = relatedEntityCount.ipCount
nodes.push(ipNode)
}
if (relatedEntityCount.subDomainCount) {
subdomainNode.data.count = relatedEntityCount.subDomainCount
nodes.push(subdomainNode)
}
if (relatedEntityCount.appCount) {
appNode.data.count = relatedEntityCount.appCount
nodes.push(appNode)
}
break
}
case 'app': {
if (relatedEntityCount.ipCount) {
ipNode.data.count = relatedEntityCount.ipCount
nodes.push(ipNode)
}
if (relatedEntityCount.domainCount) {
domainNode.data.count = relatedEntityCount.domainCount
nodes.push(domainNode)
}
break
}
}
return nodes
},
async queryRelatedEntityCount (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
console.error(e)
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
})
if (response.data && response.data.code === 200) {
return response.data.data
} else {
console.error(response)
this.showError = true
this.errorMsg = this.errorMsgHandler(response)
return null
}
},
generateLines (source, targets) {
const lines = []
targets.forEach(t => {
lines.push({
from: source.id,
to: t.id,
color: '#BEBEBE'
})
})
return lines
},
onCloseBlock () {
// todo 关闭右侧graph面板
this.mode = ''
@@ -92,6 +224,31 @@ export default {
// 鼠标移动反馈
}
}
},
async mounted () {
if (this.entity.entityType && this.entity.entityName) {
await this.init()
}
},
setup () {
const route = useRoute()
const { entityType, entityName } = route.query
const entity = {
entityType,
entityName
}
const mode = ref('ipList') // ipList, ipDetail, domainList, domainDetail, appList, appDetail
// echarts关系图的节点和连线
const graphData = ref({})
// 从url获取实体类型、名称若为空直接报错
const myChart = shallowRef(null)
return {
graphData,
entity,
myChart,
mode
}
}
}
</script>
@@ -101,6 +258,81 @@ export default {
.entity-graph__chart {
width: calc(100% - 360px);
.graph-node {
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent !important;
.graph-node__text {
width: 120px;
font-size: 12px;
color: #353636;
}
&.graph-node--root {
padding-top: 18px;
width: 82px;
height: 82px;
border: 5px solid !important;
// 覆盖自带的node点击效果
&.rel-node-checked {
box-shadow: none;
animation: none;
}
&.graph-node--ip {
border-color: #CBD9BB !important;
box-shadow: 0 0 0 8px #EDF1E7;
i {
color: #7E9F54;
}
}
&.graph-node--domain {
border-color: #AFDEED !important;
box-shadow: 0 0 0 8px #E3F3F9;
i {
color: #38ACD2;
}
}
&.graph-node--app {
border-color: #F5DAA3 !important;
box-shadow: 0 0 0 8px #FBF2DF;
i {
color: #E5A219;
}
}
i {
font-size: 36px;
}
.graph-node__text {
padding-top: 30px;
}
}
&.graph-node--primary {
padding-top: 20px;
width: 66px;
height: 66px;
border: 1px solid #A7B0B9 !important;
box-shadow: none;
transition: linear all .2s;
// 覆盖自带的node点击效果
&.rel-node-checked {
box-shadow: 0 0 0 5px #E9E9E9;
animation: none;
border-color: #778391 !important;
}
i {
font-size: 24px;
color: #778391;
}
.graph-node__text {
padding-top: 24px;
}
}
}
}
.entity-graph__detail {

View File

@@ -130,7 +130,7 @@ export default {
path: '/entityDetail',
query: {
entityType: this.entityData.entityType,
name: this.entityData.ipAddr || this.entityData.domainName || this.entityData.appName
entityName: this.entityData.ipAddr || this.entityData.domainName || this.entityData.appName
}
})
window.open(href, '_blank')