CN-1087 feat: 关系图部分实现

This commit is contained in:
chenjinsong
2023-06-30 18:43:02 +08:00
parent 6d6f863ae7
commit 0789dbcbfb
2 changed files with 249 additions and 79 deletions

View File

@@ -255,7 +255,14 @@ export const api = {
informationAggregation: apiVersion + '/entity/detail/kb/intelligence/list', informationAggregation: apiVersion + '/entity/detail/kb/intelligence/list',
// 实体关系 // 实体关系
entityGraph: { entityGraph: {
relatedEntityCount: apiVersion + '/entity/graph/relation/summaryCount' relatedEntityCount: apiVersion + '/entity/graph/relation/summaryCount',
domainRelatedIp: apiVersion + '/entity/graph/relation/domain/relate/ips',
domainRelatedApp: apiVersion + '/entity/graph/relation/domain/relate/apps',
domainRelatedSubdomain: apiVersion + '/entity/graph/relation/domain/relate/subdomains',
ipRelatedDomain: apiVersion + '/entity/graph/relation/ip/relate/domains',
ipRelatedApp: apiVersion + '/entity/graph/relation/ip/relate/apps',
appRelatedIp: apiVersion + '/entity/graph/relation/app/relate/ips',
appRelatedDomain: apiVersion + '/entity/graph/relation/app/relate/domains'
} }
} }
} }

View File

@@ -11,9 +11,12 @@
<!-- primary节点 --> <!-- primary节点 -->
<template v-else-if="node.data.level && node.data.level.length === 1"> <template v-else-if="node.data.level && node.data.level.length === 1">
<i :class="node.data.iconClass"></i> <i :class="node.data.iconClass"></i>
<div class="graph-node__text">{{node.text}}</div> <div class="graph-node__text">{{node.text}}({{node.data.count}})</div>
</template> </template>
<!-- entity节点 --> <!-- entity节点 -->
<template v-else-if="node.data.level && node.data.level.length === 3">
<i :class="node.data.iconClass"></i>
</template>
</template> </template>
</relation-graph> </relation-graph>
</div> </div>
@@ -63,7 +66,9 @@ export default {
}, },
data () { data () {
return { return {
chartOption: {} chartOption: {
debug: false
}
} }
}, },
methods: { methods: {
@@ -72,29 +77,36 @@ export default {
* 1.类型:根节点 root状态ip、app、domain; * 1.类型:根节点 root状态ip、app、domain;
* 2.类型:普通节点 primary状态normal、active; * 2.类型:普通节点 primary状态normal、active;
* 3.类型:实体 entity状态1ip、app、domain状态2normal、active. * 3.类型:实体 entity状态1ip、app、domain状态2normal、active.
*
* ip基色#7E9F54domain基色#38ACD2app基色E5A219 * ip基色#7E9F54domain基色#38ACD2app基色E5A219
*
* 为方便在逻辑上区分实体列表(圆圈节点)和实体(纯图标节点),将层级分为整层和半层两种,实体列表为整层,实体为半层。
* */ * */
this.graphData.rootId = this.entity.entityName this.graphData.rootId = this.entity.entityName
// 初始化时加载到2层半
const rootNode = this.generateRootNode() const rootNode = this.generateRootNode()
const secondLevelNodes = await this.generateSecondLevelNodes() const secondLevelNodes = await this.generateSecondLevelNodes(rootNode)
this.graphData.nodes = [rootNode, ...secondLevelNodes] const secondHalfLevelNodes = await this.generateHalfLevelNodes(secondLevelNodes)
this.graphData.lines = this.generateLines(rootNode, secondLevelNodes) this.graphData.nodes = [rootNode, ...secondLevelNodes, ...secondHalfLevelNodes]
console.info(this.graphData) this.graphData.lines = this.generateLines(rootNode)
this.graphData.lines.push(...this.generateLines(secondLevelNodes))
console.info(JSON.stringify(this.graphData))
this.$refs.relationGraph.setJsonData(this.graphData) this.$refs.relationGraph.setJsonData(this.graphData)
}, },
generateRootNode () { generateRootNode () {
let nodeIconClass = '' let iconClass = ''
switch (this.entity.entityType) { switch (this.entity.entityType) {
case 'ip': { case 'ip': {
nodeIconClass = 'cn-icon cn-icon-resolve-ip' iconClass = 'cn-icon cn-icon-resolve-ip'
break break
} }
case 'domain': { case 'domain': {
nodeIconClass = 'cn-icon cn-icon-domain1' iconClass = 'cn-icon cn-icon-domain1'
break break
} }
case 'app': { case 'app': {
nodeIconClass = 'cn-icon cn-icon-app-name' iconClass = 'cn-icon cn-icon-app-name'
break break
} }
} }
@@ -103,44 +115,97 @@ export default {
text: this.entity.entityName, text: this.entity.entityName,
nodeShape: 0, nodeShape: 0,
styleClass: `graph-node graph-node--root graph-node--${this.entity.entityType}`, styleClass: `graph-node graph-node--root graph-node--${this.entity.entityType}`,
data: { level: '1', iconClass: nodeIconClass } data: { level: '1', iconClass }
} }
}, },
generatePrimaryNode (props) { generatePrimaryNode (props) {
const node = { const node = {
styleClass: `graph-node graph-node--primary graph-node--${this.entity.entityType}` styleClass: 'graph-node graph-node--primary'
} }
return { return {
...node, ...node,
...props ...props
} }
}, },
async generateSecondLevelNodes () { generateEntityNode (level, relatedType, data) {
let iconClass = ''
switch (relatedType) {
case 'ip': {
iconClass = 'cn-icon cn-icon-resolve-ip'
break
}
case 'domain': {
iconClass = 'cn-icon cn-icon-subdomain'
break
}
case 'app': {
iconClass = 'cn-icon cn-icon-app-name'
break
}
}
return {
id: data.vertex,
text: data.vertex,
styleClass: `graph-node graph-node--entity graph-node--${relatedType}`,
data: {
level,
iconClass,
entityName: data.vertex,
entityType: relatedType
}
}
},
async generateSecondLevelNodes (rootNode) {
/* /*
* 2级及以上的整数节点先查数量大于0的才展示 * 2级及以上的整数节点先查数量大于0的才展示
* */ * */
const relatedEntityCount = await this.queryRelatedEntityCount(this.entity.entityType, this.entity.entityName) const relatedEntityCount = await this.queryRelatedEntityCount(this.entity.entityType, this.entity.entityName)
const nodes = []
if (relatedEntityCount) {
const ipNode = this.generatePrimaryNode({ const ipNode = this.generatePrimaryNode({
id: 'ip-1', id: 'ip-1',
text: this.$t('entities.graph.resolveIp'), text: this.$t('entities.graph.resolveIp'),
data: { level: '2', iconClass: 'cn-icon cn-icon-resolve-ip' } data: {
level: '2',
iconClass: 'cn-icon cn-icon-resolve-ip',
entityName: this.entity.entityName,
entityType: this.entity.entityType,
relatedType: 'ip'
}
}) })
const domainNode = this.generatePrimaryNode({ const domainNode = this.generatePrimaryNode({
id: 'domain-1', id: 'domain-1',
text: this.$t('entity.graph.resolveDomain'), text: this.$t('entity.graph.resolveDomain'),
data: { level: '2', iconClass: 'cn-icon cn-icon-subdomain' } data: {
level: '2',
iconClass: 'cn-icon cn-icon-subdomain',
entityName: this.entity.entityName,
entityType: this.entity.entityType,
relatedType: 'domain'
}
}) })
const subdomainNode = this.generatePrimaryNode({ const subdomainNode = this.generatePrimaryNode({
id: 'domain-1', id: 'domain-1',
text: this.$t('entities.subdomain'), text: this.$t('entities.subdomain'),
data: { level: '2', iconClass: 'cn-icon cn-icon-subdomain' } data: {
level: '2',
iconClass: 'cn-icon cn-icon-subdomain',
entityName: this.entity.entityName,
entityType: this.entity.entityType,
relatedType: 'domain'
}
}) })
const appNode = this.generatePrimaryNode({ const appNode = this.generatePrimaryNode({
id: 'app-1', id: 'app-1',
text: this.$t('entities.tab.relatedApp'), text: this.$t('entities.tab.relatedApp'),
data: { level: '2', iconClass: 'cn-icon cn-icon-app-name' } data: {
level: '2',
iconClass: 'cn-icon cn-icon-app-name',
entityName: this.entity.entityName,
entityType: this.entity.entityType,
relatedType: 'app'
}
}) })
const nodes = []
switch (this.entity.entityType) { switch (this.entity.entityType) {
case 'ip': { case 'ip': {
if (relatedEntityCount.domainCount) { if (relatedEntityCount.domainCount) {
@@ -180,8 +245,27 @@ export default {
break break
} }
} }
rootNode.data.childNodes = nodes
}
return nodes return nodes
}, },
async generateHalfLevelNodes (nodes) {
const newNodes = []
for (const node of nodes) {
const newNodes2 = []
const data = await this.queryRelatedEntity(node.data.entityType, node.data.entityName, node.data.relatedType)
if (data) {
// 生成节点
data.list.forEach(d => {
newNodes2.push(this.generateEntityNode(String(parseInt(node.data.level) + 0.5), node.data.relatedType, d))
})
// 更新源节点的已拓展数和列表
this.handleLoaded(node, data, newNodes2)
newNodes.push(...newNodes2)
}
}
return newNodes
},
async queryRelatedEntityCount (entityType, entityName) { async queryRelatedEntityCount (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => { const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
console.error(e) console.error(e)
@@ -197,17 +281,67 @@ export default {
return null return null
} }
}, },
generateLines (source, targets) { async queryRelatedEntity (entityType, entityName, relatedType) {
if (entityType === relatedType) {
// 若源type和关联type都是domain说明关联type是subdomain
relatedType = 'subdomain'
}
const response = await axios.get(`${api.entity.entityGraph[`${entityType}Related${_.upperFirst(relatedType)}`]}?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
}
},
/*
* 若只有一个参数则取参数的childNodes集合作为line终点
* 若有两个参数则以第一个参数作为line起点第二个参数作为终点
* */
generateLines (source, target) {
const lines = [] const lines = []
targets.forEach(t => {
lines.push({ if (!target) {
from: source.id, const sourceArr = []
to: t.id, if (!source.length) {
sourceArr.push(source)
} else {
sourceArr.push(...source)
}
sourceArr.forEach(s => {
if (s.data && s.data.childNodes) {
const lines2 = []
s.data.childNodes.forEach(c => {
lines2.push({
from: s.id,
to: c.id,
color: '#BEBEBE' color: '#BEBEBE'
}) })
}) })
s.lines = lines2
lines.push(...lines2)
}
})
}
return lines return lines
}, },
handleLoaded (node, data, childNodes) {
if (!node.data.loaded) {
node.data.loaded = []
}
node.data.loaded.push(...data.list)
if (!node.data.childNodes) {
node.data.childNodes = []
}
node.data.childNodes.push(...childNodes)
},
onCloseBlock () { onCloseBlock () {
// todo 关闭右侧graph面板 // todo 关闭右侧graph面板
this.mode = '' this.mode = ''
@@ -256,6 +390,9 @@ export default {
.entity-graph { .entity-graph {
display: flex; display: flex;
.rel-node-peel {
padding: 4px;
}
.entity-graph__chart { .entity-graph__chart {
width: calc(100% - 360px); width: calc(100% - 360px);
@@ -264,6 +401,7 @@ export default {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background-color: transparent !important; background-color: transparent !important;
transition: linear all .2s;
.graph-node__text { .graph-node__text {
width: 120px; width: 120px;
@@ -290,7 +428,7 @@ export default {
} }
&.graph-node--domain { &.graph-node--domain {
border-color: #AFDEED !important; border-color: #AFDEED !important;
box-shadow: 0 0 0 8px #E3F3F9; box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
i { i {
color: #38ACD2; color: #38ACD2;
} }
@@ -316,11 +454,10 @@ export default {
height: 66px; height: 66px;
border: 1px solid #A7B0B9 !important; border: 1px solid #A7B0B9 !important;
box-shadow: none; box-shadow: none;
transition: linear all .2s;
// 覆盖自带的node点击效果 // 覆盖自带的node点击效果
&.rel-node-checked { &.rel-node-checked {
box-shadow: 0 0 0 5px #E9E9E9; box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
animation: none; animation: none;
border-color: #778391 !important; border-color: #778391 !important;
} }
@@ -332,6 +469,32 @@ export default {
padding-top: 24px; padding-top: 24px;
} }
} }
&.graph-node--entity {
width: 21px;
height: 21px;
justify-content: center;
border: none !important;
&.graph-node--ip {
i {
color: #7E9F54;
}
}
&.graph-node--domain {
i {
color: #38ACD2;
}
}
&.graph-node--app {
i {
color: #E5A219;
}
}
i {
font-size: 21px;
}
}
} }
} }