CN-1087 fix: 关系图逻辑重构交互优化
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
@import 'views/entityExplorer/search/explorer-search';
|
||||
@import 'views/entityExplorer/entity-filter';
|
||||
@import 'views/entityExplorer/entity-detail';
|
||||
@import 'views/entityExplorer/entity-graph';
|
||||
@import 'views/entityExplorer/entityList/entity-list';
|
||||
@import './views/entityExplorer/entityList/card';
|
||||
@import './views/entityExplorer/entityList/row';
|
||||
@@ -86,5 +87,5 @@
|
||||
@import 'views/charts2/digitalCertificate';
|
||||
@import 'views/charts2/entityDetailBasicInfo';
|
||||
|
||||
@import "views/charts2/graphRightListBlock";
|
||||
@import "views/charts2/graphRightDetailBlock";
|
||||
@import "views/entityExplorer/graphRightListBlock";
|
||||
@import "views/entityExplorer/graphRightDetailBlock";
|
||||
|
||||
248
src/assets/css/components/views/entityExplorer/entity-graph.scss
Normal file
248
src/assets/css/components/views/entityExplorer/entity-graph.scss
Normal file
@@ -0,0 +1,248 @@
|
||||
.graph-toolbar {
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 0;
|
||||
|
||||
.toolbar__tools {
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
color: #575757;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.entity-graph {
|
||||
display: flex;
|
||||
|
||||
.entity-graph__chart {
|
||||
width: 100%;
|
||||
|
||||
.graph-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: transparent !important;
|
||||
transition: linear all .2s;
|
||||
|
||||
.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 rgba(126,159,84,0.14);
|
||||
i {
|
||||
color: #7E9F54;
|
||||
}
|
||||
}
|
||||
&.graph-node--domain {
|
||||
border-color: #AFDEED !important;
|
||||
box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
|
||||
i {
|
||||
color: #38ACD2;
|
||||
}
|
||||
}
|
||||
&.graph-node--app {
|
||||
border-color: #F5DAA3 !important;
|
||||
box-shadow: 0 0 0 8px rgba(229,162,25,0.14);
|
||||
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;
|
||||
|
||||
// 覆盖自带的node点击效果
|
||||
&.rel-node-checked {
|
||||
box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
|
||||
animation: none;
|
||||
border-color: #778391 !important;
|
||||
}
|
||||
i {
|
||||
font-size: 24px;
|
||||
color: #778391;
|
||||
}
|
||||
.graph-node__text {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-graph__right-box {
|
||||
& > div {
|
||||
left: unset !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
.entity-graph__detail {
|
||||
height: calc(100% - 100px) !important;
|
||||
top: 100px !important;
|
||||
// border-left: 1px solid #E2E5EC;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.g6-component-tooltip {
|
||||
box-shadow: -1px 1px 10px -1px rgba(205,205,205,0.85);
|
||||
|
||||
.primary-node-tooltip {
|
||||
padding: 5px;
|
||||
|
||||
.tooltip__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
color: #717171;
|
||||
font-size: 14px;
|
||||
}
|
||||
.tooltip__title {
|
||||
padding-left: 6px;
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
.tooltip__content {
|
||||
padding-top: 10px;
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
|
||||
span:first-of-type {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.entity-node-tooltip {
|
||||
width: 300px;
|
||||
padding: 5px;
|
||||
|
||||
.tooltip__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.tooltip__title {
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-icon {
|
||||
width: 3px !important;
|
||||
height: 12px !important;
|
||||
background: #38ACD2;
|
||||
border-radius: 1px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.content-tag-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.entity-tag {
|
||||
margin: 10px 9px 10px 0;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
border-radius: 2px;
|
||||
|
||||
$normal-color: #778391;
|
||||
$normal-light-color: #F7F8F9;
|
||||
$negative-color: #E26154;
|
||||
$negative-light-color: #FEF6F5;
|
||||
$positive-color: #749F4D;
|
||||
$positive-light-color: #F7FAF5;
|
||||
&.entity-tag--level-two-normal {
|
||||
border-color: $normal-color;
|
||||
color: $normal-color;
|
||||
background-color: $normal-light-color;
|
||||
}
|
||||
&.entity-tag--level-two-negative {
|
||||
border-color: $negative-color;
|
||||
color: $negative-color;
|
||||
background-color: $negative-light-color;
|
||||
}
|
||||
&.entity-tag--level-two-positive {
|
||||
border-color: $positive-color;
|
||||
color: $positive-color;
|
||||
background-color: $positive-light-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/entityGraph',
|
||||
component: () => import('@/views/entityExplorer/EntityGraph')
|
||||
component: () => import('@/views/entityExplorer/EntityGraph2')
|
||||
},
|
||||
{
|
||||
path: '/detection',
|
||||
|
||||
924
src/views/entityExplorer/EntityGraph2.vue
Normal file
924
src/views/entityExplorer/EntityGraph2.vue
Normal file
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="entity-graph">
|
||||
<div class="entity-graph__chart" id="entityGraph">
|
||||
</div>
|
||||
<div class="entity-graph__right-box">
|
||||
<el-drawer
|
||||
v-model="rightBox.show"
|
||||
direction="rtl"
|
||||
custom-class="entity-graph__detail"
|
||||
:close-on-click-modal="true"
|
||||
:modal="false"
|
||||
:size="400"
|
||||
:with-header="false"
|
||||
destroy-on-close>
|
||||
<graph-entity-list
|
||||
v-if="rightBox.mode === 'list'"
|
||||
:node="rightBox.node"
|
||||
:loading="rightBox.loading"
|
||||
@expandList="expandList"
|
||||
@closeBlock="onCloseBlock"
|
||||
>
|
||||
</graph-entity-list>
|
||||
<graph-entity-detail
|
||||
v-else-if="rightBox.mode === 'detail'"
|
||||
:node="rightBox.node"
|
||||
:loading="rightBox.loading"
|
||||
@expandDetailList="expandDetailList"
|
||||
>
|
||||
</graph-entity-detail>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GraphEntityList from '@/views/entityExplorer/entityGraph/GraphEntityList'
|
||||
import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDetail'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import G6, { Algorithm } from '@antv/g6'
|
||||
import Node, { nodeType, queryRelatedEntity } from './entityGraph/node'
|
||||
import Edge from './entityGraph/edge'
|
||||
|
||||
export default {
|
||||
name: 'EntityRelationship',
|
||||
components: {
|
||||
GraphEntityList,
|
||||
GraphEntityDetail
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
chartOption: {
|
||||
container: 'entityGraph',
|
||||
layout: {
|
||||
type: 'force',
|
||||
preventOverlap: true,
|
||||
linkDistance: (d) => {
|
||||
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
|
||||
return 200
|
||||
}
|
||||
return 80
|
||||
},
|
||||
nodeStrength: (d) => {
|
||||
if (d.type === nodeType.rootNode || d.type === nodeType.listNode) {
|
||||
return -100
|
||||
}
|
||||
return -10
|
||||
},
|
||||
edgeStrength: (d) => {
|
||||
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
|
||||
return 0.1
|
||||
}
|
||||
return 0.7
|
||||
}
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-nodes', 'click-select', 'zoom-canvas']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init () {
|
||||
this.registerElements() // 注册自定义node
|
||||
const tooltip = this.buildTooltip() // tooltip组件
|
||||
const toolbar = this.buildToolbar() // 工具栏组装件
|
||||
this.chartOption.plugins = [tooltip] // 注册组件
|
||||
this.graph = new G6.Graph(this.chartOption)
|
||||
const initialData = await this.generateInitialData()// 初始化数据
|
||||
this.graph.data(initialData)
|
||||
this.graph.render()
|
||||
const rootNode = this.graph.findById(this.entity.entityName)
|
||||
this.bindEvents() // 绑定事件
|
||||
this.graph.emit('node:click', { item: rootNode, target: rootNode.getKeyShape() }) // 手动触发rootNode的点击事件
|
||||
},
|
||||
registerElements () {
|
||||
const _this = this
|
||||
G6.registerNode(
|
||||
'rootNode',
|
||||
{
|
||||
draw (cfg, group) {
|
||||
group.addShape('circle', {
|
||||
name: 'rootSelectedShadow',
|
||||
attrs: {
|
||||
r: 25, // selected时38.5
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: getRootNodeStyle(cfg.myData.entityType).selectedShadowColor
|
||||
}
|
||||
})
|
||||
group.addShape('circle', {
|
||||
name: 'rootShadow',
|
||||
attrs: {
|
||||
r: 31.5,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fill: getRootNodeStyle(cfg.myData.entityType).shadowColor
|
||||
}
|
||||
})
|
||||
const node = group.addShape('circle', {
|
||||
name: 'rootInner',
|
||||
attrs: {
|
||||
r: 26,
|
||||
x: 0,
|
||||
y: 0,
|
||||
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)
|
||||
})
|
||||
} else {
|
||||
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
|
||||
selectedShadowShape.attr('r', 25)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'single-node'
|
||||
)
|
||||
G6.registerNode(
|
||||
'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,
|
||||
x: 0,
|
||||
y: 0,
|
||||
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)
|
||||
}
|
||||
}
|
||||
} 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', 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'single-node'
|
||||
)
|
||||
G6.registerNode(
|
||||
'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'
|
||||
})
|
||||
},
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'single-node'
|
||||
)
|
||||
G6.registerNode(
|
||||
'tempNode',
|
||||
{
|
||||
draw (cfg, group) {
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
text: cfg.label,
|
||||
x: 0,
|
||||
y: 25,
|
||||
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,避免鼠标事件导致重新加载图片
|
||||
}
|
||||
},
|
||||
'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)'
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
function getEntityNodeStyle (entityType) {
|
||||
switch (entityType) {
|
||||
case 'ip': {
|
||||
return {
|
||||
iconWidth: 22,
|
||||
iconHeight: 20,
|
||||
selectedShadowColor: 'rgba(126,159,84,0.1)'
|
||||
}
|
||||
}
|
||||
case 'domain': {
|
||||
return {
|
||||
iconWidth: 22,
|
||||
iconHeight: 22,
|
||||
selectedShadowColor: 'rgba(56,172,210,0.1)'
|
||||
}
|
||||
}
|
||||
case 'app': {
|
||||
return {
|
||||
iconWidth: 21,
|
||||
iconHeight: 24,
|
||||
selectedShadowColor: 'rgba(229,162,25,0.1)'
|
||||
}
|
||||
}
|
||||
}
|
||||
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)'
|
||||
}
|
||||
}
|
||||
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`)
|
||||
}
|
||||
},
|
||||
bindEvents () {
|
||||
const _this = this
|
||||
this.graph.on('node:dragstart', (e) => {
|
||||
_this.graph.layout()
|
||||
refreshDraggedNodePosition(e)
|
||||
})
|
||||
this.graph.on('node:drag', function (e) {
|
||||
refreshDraggedNodePosition(e)
|
||||
})
|
||||
this.graph.on('node:dragend', function (e) {
|
||||
e.item.get('model').fx = null
|
||||
e.item.get('model').fy = null
|
||||
})
|
||||
this.graph.on('node:click', async function (e) {
|
||||
const node = e.item
|
||||
const nodeModel = node.get('model')
|
||||
if (nodeModel.type !== 'tempNode') {
|
||||
cleanTempNodesAndTempEdges()
|
||||
// 点击entityNode,查询数据,并根据数据生成tempNode
|
||||
if (nodeModel.type === nodeType.entityNode) {
|
||||
_this.rightBox.mode = 'detail'
|
||||
// 若已查过数据,不重复查询
|
||||
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,
|
||||
// TODO k2-k1=1+k1k2
|
||||
...generateTempNodeCoordinate(nodeModel.sourceNode, e)
|
||||
},
|
||||
nodeModel
|
||||
)
|
||||
const tempEdge = new Edge(nodeModel, tempNode, 'temp')
|
||||
_this.graph.addItem('node', tempNode)
|
||||
_this.graph.addItem('edge', tempEdge)
|
||||
}
|
||||
}
|
||||
})
|
||||
change && _this.graph.layout()
|
||||
} else if (nodeModel.type === nodeType.listNode) {
|
||||
_this.rightBox.mode = 'list'
|
||||
} else if (nodeModel.type === nodeType.rootNode) {
|
||||
_this.rightBox.mode = 'detail'
|
||||
}
|
||||
_this.rightBox.node = _.cloneDeep(nodeModel)
|
||||
} 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) {
|
||||
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))
|
||||
})
|
||||
} else {
|
||||
// TODO 提示大于5层
|
||||
}
|
||||
nodes.forEach(n => {
|
||||
_this.graph.addItem('node', n)
|
||||
})
|
||||
edges.forEach(edge => {
|
||||
_this.graph.addItem('edge', edge)
|
||||
})
|
||||
cleanTempNodesAndTempEdges()
|
||||
_this.graph.layout()
|
||||
}
|
||||
|
||||
node.setState('mySelected', true)
|
||||
})
|
||||
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
|
||||
}
|
||||
function cleanTempNodesAndTempEdges () {
|
||||
// 清除现有tempNode和tempEdge
|
||||
const tempNodes = _this.graph.findAll('node', node => node.get('model').type === nodeType.tempNode)
|
||||
tempNodes.forEach(n => {
|
||||
_this.graph.removeItem(n, false)
|
||||
})
|
||||
const tempEdges = _this.graph.findAll('edge', edge => edge.get('model').isTemp)
|
||||
tempEdges.forEach(n => {
|
||||
_this.graph.removeItem(n, false)
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
async generateInitialData () {
|
||||
const nodes = []
|
||||
const edges = []
|
||||
// 生成rootNode,并查询相关数据
|
||||
const rootNode = new Node(nodeType.rootNode, this.entity.entityName, {
|
||||
entityType: this.entity.entityType,
|
||||
entityName: this.entity.entityName
|
||||
})
|
||||
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`, { entityType: k }, 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
|
||||
}
|
||||
},
|
||||
getNodeLevel (id) {
|
||||
const { findShortestPath } = Algorithm
|
||||
const info = findShortestPath(this.graph.save(), this.entity.entityName, id)
|
||||
return info.length
|
||||
},
|
||||
onCloseBlock () {
|
||||
// todo 关闭右侧graph面板
|
||||
this.rightBox.mode = ''
|
||||
this.rightBox.show = false
|
||||
},
|
||||
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) {
|
||||
let iconClass = ''
|
||||
let title = ''
|
||||
let total = 0
|
||||
let loadedCount = 0
|
||||
switch (node.myData.entityType) {
|
||||
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
|
||||
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
|
||||
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
|
||||
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')}: ${total}</span>
|
||||
<span>${_this.$t('entity.graph.expandedEntityCount')}: ${loadedCount}</span>
|
||||
</div>
|
||||
</div>`
|
||||
} else if (node.type === nodeType.entityNode || node.type === nodeType.rootNode) {
|
||||
if (node.myData && node.myData.tags && node.myData.tags.length > 0) {
|
||||
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>
|
||||
</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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
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'><i class="cn-icon cn-icon-zoom-in"></i></li>
|
||||
<li code='zoomIn'><i class="cn-icon cn-icon-zoom-out"></i></li>
|
||||
<li code='undo'><i class="cn-icon cn-icon-next-step"></i></li>
|
||||
<li code='redo'><i class="cn-icon cn-icon-pre-step"></i></li>
|
||||
<li code='autoZoom'><i class="cn-icon cn-icon-auto-zoom"></i></li>
|
||||
</ul>`
|
||||
},
|
||||
handleClick: (code, graph) => {
|
||||
toolbar.handleDefaultOperator(code)
|
||||
}
|
||||
})
|
||||
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 = sourceModel.myData.entityType
|
||||
if (sourceModel.myData.relatedEntity[expandType].list.length < 50) {
|
||||
const entities = await queryRelatedEntity(sourceModel, expandType)
|
||||
sourceModel.myData.relatedEntity[expandType].list.push(...entities.list)
|
||||
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)
|
||||
this.graph.addItem('node', entityNodeModel)
|
||||
const edge = new Edge(model, entityNodeModel)
|
||||
this.graph.addItem('edge', edge)
|
||||
this.graph.layout()
|
||||
this.graph.setItemState(this.graph.findById(edge.id), 'mySelected', true)
|
||||
})
|
||||
this.rightBox.node = _.cloneDeep(model)
|
||||
} else {
|
||||
// TODO 提示超过50
|
||||
}
|
||||
},
|
||||
async expandDetailList (nodeId, expandType) {
|
||||
const node = this.graph.findById(nodeId)
|
||||
if (node) {
|
||||
const nodeModel = node.getModel()
|
||||
if (nodeModel.myData.relatedEntity[expandType].list.length < 50) {
|
||||
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)
|
||||
entities.list.forEach(entity => {
|
||||
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
|
||||
entityType: expandType,
|
||||
entityName: entity.vertex,
|
||||
x: listNode.getModel().x + Math.random() * 500 - 250,
|
||||
y: listNode.getModel().y + Math.random() * 500 - 250
|
||||
}, listNode.getModel())
|
||||
this.graph.addItem('node', entityNodeModel)
|
||||
this.graph.addItem('edge', new Edge(listNode.getModel(), entityNodeModel))
|
||||
this.graph.layout()
|
||||
})
|
||||
this.rightBox.node = _.cloneDeep(nodeModel)
|
||||
} else {
|
||||
// TODO 提示超过50
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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 graph = shallowRef(null)
|
||||
|
||||
const rightBox = ref({
|
||||
mode: 'detail', // list | detail
|
||||
show: true,
|
||||
node: null,
|
||||
loading: false
|
||||
})
|
||||
return {
|
||||
entity,
|
||||
rightBox,
|
||||
graph
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
418
src/views/entityExplorer/entityGraph/GraphEntityDetail.vue
Normal file
418
src/views/entityExplorer/entityGraph/GraphEntityDetail.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<!--title-->
|
||||
<div class="graph-detail-basic-info">
|
||||
<div style="display: flex">
|
||||
<div class="graph-detail__icon"><i :class="iconClass"></i></div>
|
||||
|
||||
<div class="graph-detail-header">
|
||||
<div class="entity-graph-type">{{ $_.get(node, 'myData.entityType') ? entityType[$_.get(node, 'myData.entityType')] : '-' }}</div>
|
||||
<div class="graph-basic-info">
|
||||
<div class="graph-basic-info-name__block">
|
||||
<div class="graph-basic-info-name" :title="$_.get(node, 'id', '')" id="entityName">{{ $_.get(node, 'id', '') }}</div>
|
||||
<div class="graph-basic-info-icon" @click="copyEntityName">
|
||||
<i class="cn-icon cn-icon-copy"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
|
||||
</div>
|
||||
|
||||
<!--basic info-->
|
||||
<div class="digital-certificate graph-basic-info__block">
|
||||
<div class="digital-certificate-header padding-b-10">
|
||||
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||
<div class="graph-basic-info__block-title">
|
||||
{{ $t('overall.basicInfo') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-basic-info__block-content">
|
||||
<div class="graph-content-item" v-for="detail in detailCards" :key="detail.name">
|
||||
<div class="graph-content-item-label">{{ detail.label }}:</div>
|
||||
<div class="graph-content-item-value">{{ detail.value || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--关系拓展-->
|
||||
<div class="digital-certificate graph-basic-info__block">
|
||||
<loading :loading="loading" size="small"></loading>
|
||||
<div class="digital-certificate-header padding-b-10">
|
||||
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||
<div class="graph-basic-info__block-title">
|
||||
{{ $t('entity.graph.relationshipExpansion') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-basic-info__block-content" style="margin-top: -4px;">
|
||||
<div v-for="item in relationList" :key="item.name">
|
||||
<div class="graph-content-item graph-content-relationship-item" v-if="item.value === item.total">
|
||||
<div class="graph-relationship-item-label">
|
||||
<i class="margin-r-6" :class="item.icon"></i>
|
||||
<span>{{ item.label }}:</span>
|
||||
</div>
|
||||
<div class="graph-relationship-item-value">
|
||||
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
|
||||
<i class="cn-icon cn-icon-expand-relationship" :style="{color: iconColor(item.value, item.total)}"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="graph-content-item graph-content-relationship-item">
|
||||
<div class="graph-relationship-item-label">
|
||||
<i class="graph-relationship-item-label-icon margin-r-6" :class="item.icon"></i>
|
||||
<span>{{ item.label }}:</span>
|
||||
</div>
|
||||
<div class="graph-relationship-item-value">
|
||||
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
|
||||
<i class="cn-icon cn-icon-expand-relationship graph-expand-relationship__icon" :style="{color: iconColor(item.value, item.total)}" @click="expandRelation(item.name)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--标签-->
|
||||
<div v-if="$_.get(node, 'myData.tags', []).length > 0" class="digital-certificate graph-basic-info__block">
|
||||
<div class="digital-certificate-header padding-b-10">
|
||||
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||
<div class="graph-basic-info__block-title">
|
||||
{{ $t('entity.graph.tags') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-detail graph-basic-info__block-content">
|
||||
<div class="graph-tag-list">
|
||||
<div v-for="ic in $_.get(node, 'myData.tags', [])" :key="ic.value">
|
||||
<div class="entity-tag graph-tag-item" :class="`entity-tag--level-two-${ic.type}`">
|
||||
<span>{{ic.value}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { copySelectionText, selectElementText } from '@/utils/tools'
|
||||
import { entityType, riskLevelMapping } from '@/utils/constants'
|
||||
import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import { dateFormatByAppearance } from '@/utils/date-util'
|
||||
import { ref } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import Loading from '@/components/common/Loading'
|
||||
|
||||
export default {
|
||||
name: 'GraphEntityDetail',
|
||||
props: {
|
||||
node: {
|
||||
type: Object
|
||||
},
|
||||
loading: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
mixins: [chartMixin],
|
||||
components: {
|
||||
Loading
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
entityType
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appRisk () {
|
||||
return function (level) {
|
||||
const m = riskLevelMapping.find(mapping => {
|
||||
return mapping.value == level
|
||||
})
|
||||
return (m && m.name) || level
|
||||
}
|
||||
},
|
||||
iconClass () {
|
||||
let className
|
||||
switch (_.get(this.node, 'myData.entityType', '')) {
|
||||
case ('ip'): {
|
||||
className = 'cn-icon cn-icon-ip2'
|
||||
break
|
||||
}
|
||||
case ('domain'): {
|
||||
className = 'cn-icon cn-icon-domain2'
|
||||
break
|
||||
}
|
||||
case ('app'): {
|
||||
className = 'cn-icon cn-icon-app2'
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return className
|
||||
},
|
||||
handleDate () {
|
||||
return function (key) {
|
||||
const date = _.get(this.node, key, '')
|
||||
return date ? dateFormatByAppearance(date) : '-'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
node: {
|
||||
deep: true,
|
||||
handler (n) {
|
||||
this.handleDetailData(n)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.handleDetailData(this.node)
|
||||
},
|
||||
setup (props) {
|
||||
const detailCards = ref([])
|
||||
const relationList = ref([])
|
||||
const tagList = ref([])
|
||||
|
||||
return {
|
||||
detailCards,
|
||||
relationList,
|
||||
tagList
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 复制实体名称 */
|
||||
copyEntityName () {
|
||||
selectElementText(document.getElementById('entityName'))
|
||||
if (copySelectionText()) {
|
||||
this.$message.success(this.$t('tip.copySuccess'))
|
||||
} else {
|
||||
this.$message.error('Unknown error')
|
||||
}
|
||||
},
|
||||
/** 修改关系拓展图标颜色,全部拓展浅灰色,否则深灰色 */
|
||||
iconColor (length, total) {
|
||||
if (length < total) {
|
||||
if (length === 50) {
|
||||
return 'rgba(57, 57, 57, 0.5)'
|
||||
} else {
|
||||
return 'rgba(57, 57, 57, 1)'
|
||||
}
|
||||
} else {
|
||||
return 'rgba(57, 57, 57, 0.5)'
|
||||
}
|
||||
},
|
||||
// 关闭右侧详情栏
|
||||
closeBlock () {
|
||||
this.$emit('closeBlock')
|
||||
},
|
||||
/** 构造地址,国-省市-市 */
|
||||
handleLocation (data) {
|
||||
const location = []
|
||||
if (data.country) {
|
||||
location.push(data.country)
|
||||
}
|
||||
if (data.province) {
|
||||
location.push(data.province)
|
||||
}
|
||||
if (data.city) {
|
||||
location.push(data.city)
|
||||
}
|
||||
return location.join(' - ')
|
||||
},
|
||||
/** 关系拓展 */
|
||||
expandRelation (name) {
|
||||
this.$emit('expandDetailList', this.node.id, name)
|
||||
},
|
||||
httpError (e) {
|
||||
this.isNoData = false
|
||||
this.showError = true
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
},
|
||||
location (detailData) {
|
||||
let location = ''
|
||||
if (detailData) {
|
||||
const data = detailData.basicInfo.location
|
||||
if (data) {
|
||||
if (data.city) {
|
||||
location = data.city
|
||||
}
|
||||
if (data.province) {
|
||||
location = location ? `${data.province}, ${location}` : data.province
|
||||
}
|
||||
if (data.country) {
|
||||
location = location ? `${data.country}, ${location}` : data.country
|
||||
}
|
||||
}
|
||||
}
|
||||
return location || '-'
|
||||
},
|
||||
handleDetailData (node) {
|
||||
const n = node
|
||||
const type = _.get(n, 'myData.entityType', '')
|
||||
switch (type) {
|
||||
case 'ip': {
|
||||
this.detailCards = [
|
||||
{ name: 'asn', label: this.$t('entities.asNumber'), value: _.get(n, 'myData.basicInfo.asn.asn', '-') },
|
||||
{
|
||||
name: 'asOrg',
|
||||
label: this.$t('entities.asOrg'),
|
||||
value: _.get(n.myData, 'basicInfo.asn.organization', '-')
|
||||
},
|
||||
{
|
||||
name: 'isp',
|
||||
label: this.$t('entities.graph.isp'),
|
||||
value: _.get(n.myData, 'basicInfo.location.isp', '-')
|
||||
},
|
||||
{ name: 'location', label: this.$t('overall.location'), value: this.location(n.myData) }
|
||||
]
|
||||
this.relationList = [
|
||||
{
|
||||
icon: 'cn-icon cn-icon-subdomain',
|
||||
name: 'domain',
|
||||
label: this.$t('entity.graph.resolveDomain'),
|
||||
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.domain.total', 0) || 0
|
||||
},
|
||||
{
|
||||
icon: 'cn-icon cn-icon-app-name',
|
||||
name: 'app',
|
||||
label: this.$t('entities.tab.relatedApp'),
|
||||
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.app.total', '0') || 0
|
||||
}
|
||||
]
|
||||
break
|
||||
}
|
||||
case 'domain': {
|
||||
const expireDate = _.get(n.myData, 'basicInfo.whois.expireDate', '')
|
||||
const createDate = _.get(n.myData, 'basicInfo.whois.createDate', '')
|
||||
this.detailCards = [
|
||||
{
|
||||
name: 'categoryName',
|
||||
label: this.$t('entities.category'),
|
||||
value: _.get(n.myData, 'basicInfo.category.name', '-')
|
||||
},
|
||||
{
|
||||
name: 'categoryGroup',
|
||||
label: this.$t('entities.group'),
|
||||
value: _.get(n.myData, 'basicInfo.category.group', '-')
|
||||
},
|
||||
{
|
||||
name: 'reputationLevel',
|
||||
label: this.$t('entities.creditLevel2'),
|
||||
value: _.get(n.myData, 'basicInfo.category.reputationLevel', '-')
|
||||
},
|
||||
{
|
||||
name: 'expireDate',
|
||||
label: this.$t('entities.graph.expirationDate'),
|
||||
value: expireDate ? dateFormatByAppearance(expireDate) : '-'
|
||||
},
|
||||
{
|
||||
name: 'registrarName',
|
||||
label: this.$t('entities.registrar'),
|
||||
value: _.get(n.myData, 'basicInfo.whois.registrarName', '-')
|
||||
},
|
||||
{
|
||||
name: 'registrantOrg',
|
||||
label: this.$t('entities.registry'),
|
||||
value: _.get(n.myData, 'basicInfo.whois.registrantOrg', '-')
|
||||
},
|
||||
{
|
||||
name: 'registrantCountry',
|
||||
label: this.$t('entities.registrationCountry'),
|
||||
value: _.get(n.myData, 'basicInfo.whois.registrantCountry', '-')
|
||||
},
|
||||
{
|
||||
name: 'createDate',
|
||||
label: this.$t('entities.registrationDate'),
|
||||
value: createDate ? dateFormatByAppearance(createDate) : '-'
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: this.$t('entities.registryEmail'),
|
||||
value: _.get(n.myData, 'basicInfo.whois.email', '-')
|
||||
}
|
||||
]
|
||||
this.relationList = [
|
||||
{
|
||||
icon: 'cn-icon cn-icon-resolve-ip',
|
||||
name: 'ip',
|
||||
label: this.$t('entities.graph.resolveIp'),
|
||||
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
|
||||
},
|
||||
{
|
||||
icon: 'cn-icon cn-icon-subdomain',
|
||||
name: 'domain',
|
||||
label: this.$t('entities.subdomain'),
|
||||
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
|
||||
},
|
||||
{
|
||||
icon: 'cn-icon cn-icon-app-name',
|
||||
name: 'app',
|
||||
label: this.$t('entities.tab.relatedApp'),
|
||||
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.app.total', 0) || 0
|
||||
}
|
||||
]
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
this.detailCards = [
|
||||
{
|
||||
name: 'appCategory',
|
||||
label: this.$t('entities.category'),
|
||||
value: _.get(n.myData, 'detail.category.appCategory', '-')
|
||||
},
|
||||
{
|
||||
name: 'appSubcategory',
|
||||
label: this.$t('entities.subcategory'),
|
||||
value: _.get(n.myData, 'detail.category.appSubcategory', '-')
|
||||
},
|
||||
{
|
||||
name: 'appRisk',
|
||||
label: this.$t('entities.riskLevel'),
|
||||
value: _.get(n.myData, 'detail.category.appRisk', '-')
|
||||
},
|
||||
{
|
||||
name: 'appTechnology',
|
||||
label: this.$t('overall.technology'),
|
||||
value: _.get(n.myData, 'detail.category.appTechnology', '-')
|
||||
},
|
||||
{
|
||||
name: 'appLongname',
|
||||
label: this.$t('overall.appFullName'),
|
||||
value: _.get(n.myData, 'detail.category.appLongname', '-')
|
||||
},
|
||||
{
|
||||
name: 'appDescription',
|
||||
label: this.$t('config.dataSource.description'),
|
||||
value: _.get(n.myData, 'detail.category.appDescription', '-')
|
||||
}
|
||||
]
|
||||
|
||||
this.relationList = [
|
||||
{
|
||||
icon: 'cn-icon cn-icon-resolve-ip',
|
||||
name: 'ip',
|
||||
label: this.$t('entities.tab.relatedIp'),
|
||||
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
|
||||
},
|
||||
{
|
||||
icon: 'cn-icon cn-icon-subdomain',
|
||||
name: 'domain',
|
||||
label: this.$t('entities.graph.relatedDomain'),
|
||||
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
189
src/views/entityExplorer/entityGraph/GraphEntityList.vue
Normal file
189
src/views/entityExplorer/entityGraph/GraphEntityList.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div>
|
||||
<loading :loading="loading" size="small"></loading>
|
||||
<div class="graph-list-header">
|
||||
<div>
|
||||
<div class="graph-list-header-title">
|
||||
<i class="cn-icon cn-icon-resolve-ip graph-list-header-icon"></i>
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<div class="graph-list-header-number">
|
||||
{{ $t('entity.graph.associatedCount') }}: <span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.total', '-')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
|
||||
</div>
|
||||
|
||||
<div class="graph-list-expand-btn-block">
|
||||
<div class="graph-list-expand-btn" :class="{ 'graph-list-expand-btn--disabled': expandBtnDisable }" style="display: inline-flex;" @click="expandList">
|
||||
<i class="cn-icon cn-icon-expand-continue graph-expand-continue"></i>
|
||||
{{ $t('entity.graph.continueToExpand') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="digital-certificate">
|
||||
<div class="digital-certificate-header padding-b-16">
|
||||
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||
<div class="graph-list-content-header ">
|
||||
{{ $t('entity.graph.expandedEntityCount') }}:
|
||||
<span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-list-content">
|
||||
<div v-for="(item, index) in $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', [])" :key="index">
|
||||
<div class="graph-list-item-ip"><span @click="expandDetail">{{ item.vertex }}</span></div>
|
||||
<template v-if="$_.get(node, 'myData.entityType', '') === 'ip'">
|
||||
<div class="graph-list-item-block">
|
||||
<div class="graph-list-item padding-b-4">
|
||||
<div class="graph-list-item-label">{{ $t('overall.location') }}:</div>
|
||||
<div class="graph-list-item-value graph-list-item-value1" style="display: flex;align-items: center;">
|
||||
<!-- <img :src="require(`../../../../public/images/flag/${item.flag}.svg`)" class="graph-list-country-flag"/>-->
|
||||
<span>{{ $_.get(item.detail, 'location.city', '-') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-list-item">
|
||||
<div class="graph-list-item-label">ASN:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'asn.asn', '-') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'domain'">
|
||||
<div class="graph-list-item-block">
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.name', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.group')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.group', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.registration')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantCountry', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.registry')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantOrg', '-') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'app'">
|
||||
<div class="graph-list-item-block">
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">APP ID:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appId', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appCategory', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.subcategory')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appSubcategory', '-') }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('entities.riskLevel')}}:</div>
|
||||
<div class="graph-list-item-value">{{ appRisk($_.get(item.detail, 'category.appRisk', '-')) }}</div>
|
||||
</div>
|
||||
<div class="graph-list-item__app">
|
||||
<div class="graph-list-item-label__app">{{$t('config.dataSource.description')}}:</div>
|
||||
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appDescription', '-') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="graph-list-dividing-line" v-if="index !== $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length - 1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { riskLevelMapping } from '@/utils/constants'
|
||||
import { scrollToTop } from '@/utils/tools'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'GraphEntityList',
|
||||
props: {
|
||||
node: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Loading
|
||||
},
|
||||
computed: {
|
||||
expandBtnDisable () {
|
||||
const loaded = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.list', []).length
|
||||
const total = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.total', 0)
|
||||
return !(loaded < total && total > 10 && (loaded && loaded < 50))
|
||||
},
|
||||
appRisk () {
|
||||
return function (level) {
|
||||
const m = riskLevelMapping.find(mapping => {
|
||||
return mapping.value == level
|
||||
})
|
||||
return (m && m.name) || level
|
||||
}
|
||||
},
|
||||
title () {
|
||||
let title = ''
|
||||
if (this.node) {
|
||||
const entityType = _.get(this.node, 'myData.entityType', '')
|
||||
if (entityType) {
|
||||
const sourceType = _.get(this.node, 'sourceNode.myData.entityType', '')
|
||||
switch (entityType) {
|
||||
case 'ip': {
|
||||
if (sourceType === 'domain') {
|
||||
title = this.$t('entities.graph.resolveIp')
|
||||
} else {
|
||||
title = this.$t('entities.tab.relatedIp')
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'domain': {
|
||||
if (sourceType === 'ip') {
|
||||
title = this.$t('entity.graph.resolveDomain')
|
||||
} else if (sourceType === 'domain') {
|
||||
title = this.$t('entities.subdomain')
|
||||
} else {
|
||||
title = this.$t('entities.relatedDomain')
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
title = this.$t('entities.tab.relatedApp')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return title
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeBlock () {
|
||||
this.$emit('closeBlock')
|
||||
},
|
||||
expandDetail () {
|
||||
this.$emit('expandDetail', this.node.id)
|
||||
},
|
||||
expandList () {
|
||||
// 继续拓展ip列表,传递信息,调用接口
|
||||
this.$emit('expandList', _.get(this.node, 'id'))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
scrollToTop()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
52
src/views/entityExplorer/entityGraph/edge.js
Normal file
52
src/views/entityExplorer/entityGraph/edge.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import G6 from '@antv/g6'
|
||||
|
||||
export default class Edge {
|
||||
constructor (sourceNode, targetNode, type) {
|
||||
this.id = sourceNode.id + '__' + targetNode.id
|
||||
this.source = sourceNode.id
|
||||
this.target = targetNode.id
|
||||
this.isTemp = type === 'temp'
|
||||
this.style = type === 'temp' ? tempStyles.style : normalStyles.style
|
||||
this.stateStyles = type === 'temp' ? tempStyles.stateStyles : normalStyles.stateStyles
|
||||
}
|
||||
}
|
||||
|
||||
const normalStyles = {
|
||||
style: {
|
||||
stroke: '#BEBEBE',
|
||||
endArrow: {
|
||||
path: G6.Arrow.triangle(5, 5),
|
||||
fill: '#BEBEBE'
|
||||
}
|
||||
},
|
||||
stateStyles: {
|
||||
mySelected: {
|
||||
stroke: '#778391',
|
||||
endArrow: {
|
||||
path: G6.Arrow.triangle(5, 5),
|
||||
fill: '#778391'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tempStyles = {
|
||||
style: {
|
||||
endArrow: {
|
||||
path: G6.Arrow.triangle(5, 5),
|
||||
fill: '#DDD'
|
||||
},
|
||||
stroke: '#DDD',
|
||||
lineDash: [3, 2]
|
||||
},
|
||||
stateStyles: {
|
||||
mySelected: {
|
||||
stroke: '#DDD',
|
||||
endArrow: {
|
||||
path: G6.Arrow.triangle(5, 5),
|
||||
fill: '#DDD'
|
||||
},
|
||||
lineDash: [3, 2]
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/views/entityExplorer/entityGraph/node.js
Normal file
182
src/views/entityExplorer/entityGraph/node.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import _ from 'lodash'
|
||||
import i18n from '@/i18n'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import { entityDetailTags, psiphon3IpType } from '@/utils/constants'
|
||||
|
||||
export default class Node {
|
||||
/*
|
||||
* type: 对应nodeType
|
||||
* cfg: { entityType, entityName, x, y, fx, fy }
|
||||
* */
|
||||
constructor (type, id, cfg, sourceNode) {
|
||||
this.type = type
|
||||
this.id = id
|
||||
this.x = _.get(cfg, 'x', null)
|
||||
this.y = _.get(cfg, 'y', null)
|
||||
this.fx = _.get(cfg, 'fx', null)
|
||||
this.fy = _.get(cfg, 'fy', null)
|
||||
this.myData = {
|
||||
entityType: cfg.entityType,
|
||||
entityName: cfg.entityName
|
||||
}
|
||||
if (sourceNode) {
|
||||
this.sourceNode = sourceNode
|
||||
}
|
||||
this.label = this.generateLabel()
|
||||
}
|
||||
|
||||
generateLabel () {
|
||||
switch (this.type) {
|
||||
case nodeType.rootNode:
|
||||
case nodeType.entityNode: {
|
||||
return this.id
|
||||
}
|
||||
case nodeType.listNode: {
|
||||
return `${this.getLabelText()}(${_.get(this.sourceNode.myData, 'relatedEntity.' + this.myData.entityType + '.total', 0)})`
|
||||
}
|
||||
case nodeType.tempNode: {
|
||||
return this.getLabelText()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
getLabelText () {
|
||||
if (this.myData.entityType === 'ip') {
|
||||
if (this.sourceNode.myData.entityType === 'app') {
|
||||
return i18n.global.t('entities.tab.relatedIp')
|
||||
} else if (this.sourceNode.myData.entityType === 'domain') {
|
||||
return i18n.global.t('entities.graph.resolveIp')
|
||||
}
|
||||
} else if (this.myData.entityType === 'domain') {
|
||||
if (this.sourceNode.myData.entityType === 'ip') {
|
||||
return i18n.global.t('entities.graph.resolvedDomain')
|
||||
} else if (this.sourceNode.myData.entityType === 'app') {
|
||||
return i18n.global.t('entities.graph.relatedDomain')
|
||||
} else if (this.sourceNode.myData.entityType === 'domain') {
|
||||
return i18n.global.t('entities.subdomain')
|
||||
}
|
||||
} else if (this.myData.entityType === 'app') {
|
||||
return i18n.global.t('entities.tab.relatedApp')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
isSubdomain () {
|
||||
if (this.sourceNode) {
|
||||
return this.sourceNode.myData.entityType === 'domain' && this.myData.entityType === 'domain'
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询basicInfo、tags、关联实体数量
|
||||
async queryDetailData () {
|
||||
const entityType = this.myData.entityType
|
||||
const entityName = this.myData.entityName
|
||||
|
||||
this.myData.basicInfo = await this.queryEntityBasicInfo(entityType, entityName)
|
||||
|
||||
const tags = await this.queryTags(entityType, entityName)
|
||||
let _tags = []
|
||||
Object.keys(tags).forEach(k => {
|
||||
if (k !== 'userDefinedTags' && tags[k]) {
|
||||
Object.keys(tags[k]).forEach(k2 => {
|
||||
const find = entityDetailTags[entityType].find(t => t.name === k2)
|
||||
if (find) {
|
||||
_tags.push({ key: k2, value: this.tagValueHandler(k, k2, tags[k][k2]), type: find.type })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (_.isArray(tags.userDefinedTags)) {
|
||||
_tags = _.concat(_tags, tags.userDefinedTags.map(tag => ({ value: tag.tagValue, type: 'normal' })))
|
||||
}
|
||||
this.myData.tags = _tags
|
||||
|
||||
const relatedEntityTotalCount = await this.queryRelatedEntityCount(entityType, entityName)
|
||||
this.myData.relatedEntity = {
|
||||
ip: { total: relatedEntityTotalCount.ipCount, list: [] },
|
||||
domain: { total: this.myData.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, list: [] },
|
||||
app: { total: relatedEntityTotalCount.appCount, list: [] }
|
||||
}
|
||||
}
|
||||
|
||||
async queryEntityBasicInfo (entityType, entityName) {
|
||||
const response = await axios.get(`${api.entity.entityGraph.basicInfo}/${entityType}?resource=${entityName}`).catch(e => {
|
||||
console.error(e)
|
||||
throw e
|
||||
})
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
|
||||
async queryTags (entityType, entityName) {
|
||||
const response = await axios.get(`${api.entity.entityGraph.tags}/${entityType}?resource=${entityName}`).catch(e => {
|
||||
console.error(e)
|
||||
throw e
|
||||
})
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
|
||||
async queryRelatedEntityCount (entityType, entityName) {
|
||||
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
|
||||
console.error(e)
|
||||
throw e
|
||||
})
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
|
||||
tagValueHandler (k, k2, value) {
|
||||
if (k === 'psiphon3Ip') {
|
||||
if (k2 === 'type') {
|
||||
const find = psiphon3IpType.find(t => t.value === value)
|
||||
if (find) {
|
||||
return find.name
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
export const nodeType = {
|
||||
rootNode: 'rootNode',
|
||||
listNode: 'listNode',
|
||||
entityNode: 'entityNode',
|
||||
tempNode: 'tempNode'
|
||||
}
|
||||
export async function queryRelatedEntity (node, targetEntityType) {
|
||||
let _targetEntityType = targetEntityType
|
||||
if (node.myData.entityType === 'domain' && targetEntityType === 'domain') {
|
||||
_targetEntityType = 'subdomain'
|
||||
}
|
||||
let url = `${api.entity.entityGraph[`${node.myData.entityType}Related${_.upperFirst(_targetEntityType)}`]}?resource=${node.myData.entityName}&pageSize=10`
|
||||
const current = node.myData.relatedEntity[targetEntityType].list.length
|
||||
const pageNo = parseInt(current / 10) + 1
|
||||
url += `&pageNo=${pageNo}`
|
||||
const response = await axios.get(url).catch(e => {
|
||||
console.error(e)
|
||||
throw e
|
||||
})
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user