CN-1087 feat: 关系图部分实现
This commit is contained in:
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;状态1:ip、app、domain;状态2:normal、active.
|
* 3.类型:实体 entity;状态1:ip、app、domain;状态2:normal、active.
|
||||||
|
*
|
||||||
* ip基色#7E9F54,domain基色#38ACD2,app基色E5A219
|
* ip基色#7E9F54,domain基色#38ACD2,app基色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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user