mirror of https://github.com/veops/cmdb.git
feat: update topology view
This commit is contained in:
parent
5358fb41b2
commit
b29f498748
|
@ -1,317 +1,319 @@
|
|||
<template>
|
||||
<div>
|
||||
<div id="search-form-bar" class="search-form-bar">
|
||||
<div :style="{ display: 'inline-flex', alignItems: 'center' }">
|
||||
<a-space>
|
||||
<treeselect
|
||||
v-if="type === 'resourceSearch'"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder"
|
||||
:style="{
|
||||
width: '200px',
|
||||
marginRight: '10px',
|
||||
'--custom-height': '32px',
|
||||
'--custom-bg-color': '#fff',
|
||||
'--custom-border': '1px solid #d9d9d9',
|
||||
'--custom-multiple-lineHeight': '16px',
|
||||
}"
|
||||
v-model="currenCiType"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="ciTypeGroup"
|
||||
:limit="1"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:placeholder="$t('cmdb.ciType.ciType')"
|
||||
@close="closeCiTypeGroup"
|
||||
@open="openCiTypeGroup"
|
||||
@input="inputCiTypeGroup"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.id || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.ci_types,
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
<a-input
|
||||
v-model="fuzzySearch"
|
||||
:style="{ display: 'inline-block', width: '200px' }"
|
||||
:placeholder="$t('cmdb.components.pleaseSearch')"
|
||||
@pressEnter="emitRefresh"
|
||||
>
|
||||
<a-icon
|
||||
type="search"
|
||||
slot="suffix"
|
||||
:style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }"
|
||||
@click="emitRefresh"
|
||||
/>
|
||||
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
|
||||
<template slot="title">
|
||||
{{ $t('cmdb.components.ciSearchTips') }}
|
||||
</template>
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
</a-input>
|
||||
<a-tooltip :title="$t('reset')">
|
||||
<a-button @click="reset">{{ $t('reset') }}</a-button>
|
||||
</a-tooltip>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div slot="popover_item" class="search-form-bar-filter">
|
||||
<a-icon class="search-form-bar-filter-icon" type="filter" />
|
||||
{{ $t('cmdb.components.conditionFilter') }}
|
||||
<a-icon class="search-form-bar-filter-icon" type="down" :style="{ color: '#d9d9d9' }" />
|
||||
</div>
|
||||
</FilterComp>
|
||||
<a-input
|
||||
v-if="isShowExpression"
|
||||
v-model="expression"
|
||||
v-show="!selectedRowKeys.length"
|
||||
@focus="
|
||||
() => {
|
||||
isFocusExpression = true
|
||||
}
|
||||
"
|
||||
@blur="
|
||||
() => {
|
||||
isFocusExpression = false
|
||||
}
|
||||
"
|
||||
:class="{ 'ci-searchform-expression': true, 'ci-searchform-expression-has-value': expression }"
|
||||
:style="{ width }"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="emitRefresh"
|
||||
>
|
||||
<a-icon slot="suffix" type="check-circle" @click="handleCopyExpression" />
|
||||
</a-input>
|
||||
<slot></slot>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-space>
|
||||
<slot name="extraContent"></slot>
|
||||
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
|
||||
<a
|
||||
@click="
|
||||
() => {
|
||||
$refs.metadataDrawer.open(typeId)
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
v-if="type === 'relationView'"
|
||||
type="question-circle"
|
||||
/></a>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<MetadataDrawer ref="metadataDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Treeselect from '@riophae/vue-treeselect'
|
||||
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { getCITypeGroups } from '../../api/ciTypeGroup'
|
||||
export default {
|
||||
name: 'SearchForm',
|
||||
components: { MetadataDrawer, FilterComp, Treeselect },
|
||||
props: {
|
||||
preferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isShowExpression: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
typeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedRowKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Advanced Search Expand/Close
|
||||
advanced: false,
|
||||
queryParam: {},
|
||||
isFocusExpression: false,
|
||||
expression: '',
|
||||
fuzzySearch: '',
|
||||
currenCiType: [],
|
||||
ciTypeGroup: [],
|
||||
lastCiType: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
|
||||
},
|
||||
width() {
|
||||
return '200px'
|
||||
},
|
||||
canSearchPreferenceAttrList() {
|
||||
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': function(newValue, oldValue) {
|
||||
this.queryParam = {}
|
||||
this.expression = ''
|
||||
this.fuzzySearch = ''
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
setPreferenceSearchCurrent: {
|
||||
from: 'setPreferenceSearchCurrent',
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.type === 'resourceSearch') {
|
||||
this.getCITypeGroups()
|
||||
}
|
||||
if (this.typeId) {
|
||||
this.currenCiType = [this.typeId]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// toggleAdvanced() {
|
||||
// this.advanced = !this.advanced
|
||||
// },
|
||||
getCITypeGroups() {
|
||||
getCITypeGroups({ need_other: true }).then((res) => {
|
||||
this.ciTypeGroup = res
|
||||
.filter((item) => item.ci_types && item.ci_types.length)
|
||||
.map((item) => {
|
||||
item.id = `parent_${item.id || -1}`
|
||||
return { ..._.cloneDeep(item) }
|
||||
})
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
this.queryParam = {}
|
||||
this.expression = ''
|
||||
this.fuzzySearch = ''
|
||||
this.currenCiType = []
|
||||
this.emitRefresh()
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
if (expSort) {
|
||||
expression += `&sort=${expSort}`
|
||||
}
|
||||
this.expression = expression
|
||||
this.emitRefresh()
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.filterComp.handleSubmit()
|
||||
},
|
||||
openCiTypeGroup() {
|
||||
this.lastCiType = _.cloneDeep(this.currenCiType)
|
||||
},
|
||||
closeCiTypeGroup(value) {
|
||||
if (!_.isEqual(value, this.lastCiType)) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
inputCiTypeGroup(value) {
|
||||
if (!value || !value.length) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
emitRefresh() {
|
||||
if (this.setPreferenceSearchCurrent) {
|
||||
this.setPreferenceSearchCurrent(null)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$emit('refresh', true)
|
||||
})
|
||||
},
|
||||
handleCopyExpression() {
|
||||
this.$emit('copyExpression')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import '../../views/index.less';
|
||||
.ci-searchform-expression {
|
||||
> input {
|
||||
border-bottom: 2px solid #d9d9d9;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom: 2px solid @primary-color;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 2px 2px -2px #1f78d133;
|
||||
}
|
||||
}
|
||||
.ant-input-suffix {
|
||||
color: #d9d9d9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.ci-searchform-expression-has-value .ant-input-suffix {
|
||||
color: @func-color_3;
|
||||
}
|
||||
.cmdb-search-form {
|
||||
.ant-form-item-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-form-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
.search-form-bar-filter {
|
||||
.ops_display_wrapper(transparent);
|
||||
.search-form-bar-filter-icon {
|
||||
color: @primary-color;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<div id="search-form-bar" class="search-form-bar">
|
||||
<div :style="{ display: 'inline-flex', alignItems: 'center' }">
|
||||
<a-space>
|
||||
<treeselect
|
||||
v-if="type === 'resourceSearch'"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder"
|
||||
:style="{
|
||||
width: '200px',
|
||||
marginRight: '10px',
|
||||
'--custom-height': '32px',
|
||||
'--custom-bg-color': '#fff',
|
||||
'--custom-border': '1px solid #d9d9d9',
|
||||
'--custom-multiple-lineHeight': '16px',
|
||||
}"
|
||||
v-model="currenCiType"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="ciTypeGroup"
|
||||
:limit="1"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:placeholder="$t('cmdb.ciType.ciType')"
|
||||
@close="closeCiTypeGroup"
|
||||
@open="openCiTypeGroup"
|
||||
@input="inputCiTypeGroup"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.id || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.ci_types,
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
<a-input
|
||||
v-model="fuzzySearch"
|
||||
:style="{ display: 'inline-block', width: '200px' }"
|
||||
:placeholder="$t('cmdb.components.pleaseSearch')"
|
||||
@pressEnter="emitRefresh"
|
||||
>
|
||||
<a-icon
|
||||
type="search"
|
||||
slot="suffix"
|
||||
:style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }"
|
||||
@click="emitRefresh"
|
||||
/>
|
||||
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
|
||||
<template slot="title">
|
||||
{{ $t('cmdb.components.ciSearchTips') }}
|
||||
</template>
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
</a-input>
|
||||
<a-tooltip :title="$t('reset')">
|
||||
<a-button @click="reset">{{ $t('reset') }}</a-button>
|
||||
</a-tooltip>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div slot="popover_item" class="search-form-bar-filter">
|
||||
<a-icon class="search-form-bar-filter-icon" type="filter" />
|
||||
{{ $t('cmdb.components.conditionFilter') }}
|
||||
<a-icon class="search-form-bar-filter-icon" type="down" :style="{ color: '#d9d9d9' }" />
|
||||
</div>
|
||||
</FilterComp>
|
||||
<a-input
|
||||
v-if="isShowExpression"
|
||||
v-model="expression"
|
||||
v-show="!selectedRowKeys.length"
|
||||
@focus="
|
||||
() => {
|
||||
isFocusExpression = true
|
||||
}
|
||||
"
|
||||
@blur="
|
||||
() => {
|
||||
isFocusExpression = false
|
||||
}
|
||||
"
|
||||
:class="{ 'ci-searchform-expression': true, 'ci-searchform-expression-has-value': expression }"
|
||||
:style="{ width }"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="emitRefresh"
|
||||
>
|
||||
<a-icon slot="suffix" type="check-circle" @click="handleCopyExpression" />
|
||||
</a-input>
|
||||
<slot></slot>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-space>
|
||||
<slot name="extraContent"></slot>
|
||||
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
|
||||
<a
|
||||
@click="
|
||||
() => {
|
||||
$refs.metadataDrawer.open(typeId)
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
v-if="type === 'relationView'"
|
||||
type="question-circle"
|
||||
/></a>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<MetadataDrawer ref="metadataDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Treeselect from '@riophae/vue-treeselect'
|
||||
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { getCITypeGroups } from '../../api/ciTypeGroup'
|
||||
export default {
|
||||
name: 'SearchForm',
|
||||
components: { MetadataDrawer, FilterComp, Treeselect },
|
||||
props: {
|
||||
preferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isShowExpression: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
typeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedRowKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Advanced Search Expand/Close
|
||||
advanced: false,
|
||||
queryParam: {},
|
||||
isFocusExpression: false,
|
||||
expression: '',
|
||||
fuzzySearch: '',
|
||||
currenCiType: [],
|
||||
ciTypeGroup: [],
|
||||
lastCiType: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
placeholder() {
|
||||
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
|
||||
},
|
||||
width() {
|
||||
return '200px'
|
||||
},
|
||||
canSearchPreferenceAttrList() {
|
||||
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.path': function(newValue, oldValue) {
|
||||
this.queryParam = {}
|
||||
this.expression = ''
|
||||
this.fuzzySearch = ''
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
setPreferenceSearchCurrent: {
|
||||
from: 'setPreferenceSearchCurrent',
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.type === 'resourceSearch') {
|
||||
this.getCITypeGroups()
|
||||
}
|
||||
if (this.typeId) {
|
||||
this.currenCiType = [this.typeId]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// toggleAdvanced() {
|
||||
// this.advanced = !this.advanced
|
||||
// },
|
||||
getCITypeGroups() {
|
||||
getCITypeGroups({ need_other: true }).then((res) => {
|
||||
this.ciTypeGroup = res
|
||||
.filter((item) => item.ci_types && item.ci_types.length)
|
||||
.map((item) => {
|
||||
item.id = `parent_${item.id || -1}`
|
||||
return { ..._.cloneDeep(item) }
|
||||
})
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
this.queryParam = {}
|
||||
this.expression = ''
|
||||
this.fuzzySearch = ''
|
||||
if (this.type !== 'resourceView') {
|
||||
this.currenCiType = []
|
||||
}
|
||||
this.emitRefresh()
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
if (expSort) {
|
||||
expression += `&sort=${expSort}`
|
||||
}
|
||||
this.expression = expression
|
||||
this.emitRefresh()
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.filterComp.handleSubmit()
|
||||
},
|
||||
openCiTypeGroup() {
|
||||
this.lastCiType = _.cloneDeep(this.currenCiType)
|
||||
},
|
||||
closeCiTypeGroup(value) {
|
||||
if (!_.isEqual(value, this.lastCiType)) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
inputCiTypeGroup(value) {
|
||||
if (!value || !value.length) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
emitRefresh() {
|
||||
if (this.setPreferenceSearchCurrent) {
|
||||
this.setPreferenceSearchCurrent(null)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$emit('refresh', true)
|
||||
})
|
||||
},
|
||||
handleCopyExpression() {
|
||||
this.$emit('copyExpression')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import '../../views/index.less';
|
||||
.ci-searchform-expression {
|
||||
> input {
|
||||
border-bottom: 2px solid #d9d9d9;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom: 2px solid @primary-color;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 2px 2px -2px #1f78d133;
|
||||
}
|
||||
}
|
||||
.ant-input-suffix {
|
||||
color: #d9d9d9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.ci-searchform-expression-has-value .ant-input-suffix {
|
||||
color: @func-color_3;
|
||||
}
|
||||
.cmdb-search-form {
|
||||
.ant-form-item-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-form-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
.search-form-bar-filter {
|
||||
.ops_display_wrapper(transparent);
|
||||
.search-form-bar-filter-icon {
|
||||
color: @primary-color;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -589,6 +589,7 @@ if __name__ == "__main__":
|
|||
confirmDeleteView: 'Are you sure you want to delete this view ?',
|
||||
noInstancePerm: 'You do not have read permissions for this instance',
|
||||
noPreferenceAttributes: 'This instance has no subscription attributes or no default displayed attributes',
|
||||
topoViewSearchPlaceholder: 'Please enter the node name.'
|
||||
},
|
||||
}
|
||||
export default cmdb_en
|
||||
|
|
|
@ -589,6 +589,7 @@ if __name__ == "__main__":
|
|||
confirmDeleteView: '您确定要删除该视图吗?',
|
||||
noInstancePerm: '您没有该实例的查看权限',
|
||||
noPreferenceAttributes: '该实例没有订阅属性或者没有默认展示的属性',
|
||||
topoViewSearchPlaceholder: '请输入节点名字'
|
||||
},
|
||||
}
|
||||
export default cmdb_zh
|
||||
|
|
|
@ -140,12 +140,33 @@
|
|||
<div :style="{ height: `${windowHeight - 80}px` }" ref="rightTopoView">
|
||||
<RelationGraph ref="showTopoView" :options="graphOptions2" :on-node-click="showNodeTips">
|
||||
<template #node="{node}">
|
||||
<div :style="{ lineHeight: '20px' }">
|
||||
<ops-icon type="caise-wuliji" />
|
||||
<span :style="{ marginLeft: '5px', textOverflow: 'ellipsis' }">{{ node.text }}aaa</span>
|
||||
<div
|
||||
:style="{ borderColor: node.data.btnType === 'more' ? '#A4B5E1' : nodeStyle[Math.abs(node.lot.level)] ? nodeStyle[Math.abs(node.lot.level)].backgroundColor : '#A4B5E1' }"
|
||||
class="relation-graph-node"
|
||||
>
|
||||
<template v-if="node.data.icon">
|
||||
<img
|
||||
v-if="node.data.icon.split('$$')[2]"
|
||||
:src="`/api/common-setting/v1/file/${node.data.icon.split('$$')[3]}`"
|
||||
class="relation-graph-node-image"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: node.data.icon.split('$$')[1] }"
|
||||
:type="node.data.icon ? node.data.icon.split('$$')[0] : ''"
|
||||
class="relation-graph-node-icon"
|
||||
/>
|
||||
</template>
|
||||
<span class="relation-graph-node-text">{{ node.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #graph-plug>
|
||||
<a-input-search
|
||||
class="relation-graph-search"
|
||||
v-model="topoViewSearchValue"
|
||||
:placeholder="$t('cmdb.topo.topoViewSearchPlaceholder')"
|
||||
@search="handleSearchTopoView"
|
||||
/>
|
||||
<div v-if="(isShowNodeTipsPanel && currentNodeValues && currentNodeAttributes.length) || errorMessageShow" :style="nodeTipsPosition" class="node-tips">
|
||||
<a-descriptions
|
||||
v-if="currentNodeValues"
|
||||
|
@ -236,12 +257,12 @@
|
|||
</SeeksRelationGraph>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item :style="{ display: 'none' }" :label="$t('cmdb.topo.aggregationCount')" prop="aggregation_count" :help="$t('cmdb.topo.aggreationCountTip')">
|
||||
<a-form-item :label="$t('cmdb.topo.aggregationCount')" prop="aggregation_count" :help="$t('cmdb.topo.aggreationCountTip')">
|
||||
<a-input-number
|
||||
:style="{ width: '100%' }"
|
||||
:min="0"
|
||||
v-decorator="['aggregation_count']"
|
||||
>
|
||||
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<div :class="{ 'chart-left-preview': true, 'chart-left-preview-empty': !isShowPreview }">
|
||||
|
@ -253,9 +274,27 @@
|
|||
>
|
||||
<template v-if="isShowPreview">
|
||||
<RelationGraph ref="previewTopoView" :options="graphOptionsPrivew">
|
||||
<div slot="node" slot-scope="{ node }" :style="{ lineHeight: '20px' }">
|
||||
<span :style="{ marginLeft: '5px' }">{{ node.text }}</span>
|
||||
</div>
|
||||
<template #node="{node}">
|
||||
<div
|
||||
:style="{ borderColor: nodeStyle[node.lot.level] ? nodeStyle[node.lot.level].backgroundColor : '#7F97FA' }"
|
||||
class="relation-graph-node"
|
||||
>
|
||||
<template v-if="node.data.icon">
|
||||
<img
|
||||
v-if="node.data.icon.split('$$')[2]"
|
||||
:src="`/api/common-setting/v1/file/${node.data.icon.split('$$')[3]}`"
|
||||
class="relation-graph-node-image"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: node.data.icon.split('$$')[1] }"
|
||||
:type="node.data.icon ? node.data.icon.split('$$')[0] : ''"
|
||||
class="relation-graph-node-icon"
|
||||
/>
|
||||
</template>
|
||||
<span class="relation-graph-node-text">{{ node.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</RelationGraph>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -290,6 +329,7 @@ import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
|||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getTopoGroups, postTopoGroup, putTopoGroupByGId, putTopoGroupsOrder, deleteTopoGroup, getTopoView, addTopoView, updateTopoView, deleteTopoView, getRelationsByTypeId, previewTopoView, showTopoView } from '@/modules/cmdb/api/topology'
|
||||
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const currentTopoKey = 'ops_cmdb_topo_currentId'
|
||||
export default {
|
||||
|
@ -326,6 +366,7 @@ export default {
|
|||
const graphOptions2 = {
|
||||
// ...defaultOptions,
|
||||
backgrounImageNoRepeat: true,
|
||||
ovUseNodeSlot: true,
|
||||
placeOtherGroup: true,
|
||||
moveToCenterWhenRefresh: true,
|
||||
zoomToFitWhenRefresh: true,
|
||||
|
@ -339,10 +380,11 @@ export default {
|
|||
max_per_width: 200,
|
||||
min_per_height: 40,
|
||||
max_per_height: undefined,
|
||||
defaultLineColor: '#CACDD9',
|
||||
defaultNodeColor: '#29AAE1',
|
||||
defaultNodeFontColor: '#ffffff',
|
||||
defaultNodeBorderColor: '#b1c9ff',
|
||||
backgroundColor: '#FFFFFF',
|
||||
// defaultLineColor: '#CACDD9',
|
||||
// defaultNodeColor: '#29AAE1',
|
||||
// defaultNodeFontColor: '#ffffff',
|
||||
// defaultNodeBorderColor: '#b1c9ff',
|
||||
defaultExpandHolderPosition: 'right',
|
||||
defaultJunctionPoint: 'lr',
|
||||
layouts: [
|
||||
|
@ -350,7 +392,7 @@ export default {
|
|||
layoutName: 'tree',
|
||||
from: 'left',
|
||||
layoutClassName: 'seeks-layout-center',
|
||||
defaultExpandHolderPosition: 'hide',
|
||||
defaultExpandHolderPosition: 'right',
|
||||
defaultJunctionPoint: 'border',
|
||||
},
|
||||
],
|
||||
|
@ -411,6 +453,29 @@ export default {
|
|||
|
||||
errorMessageShow: false,
|
||||
errorMessage: '',
|
||||
nodeStyle: {
|
||||
'0': {
|
||||
backgroundColor: '#2F54EB'
|
||||
},
|
||||
'1': {
|
||||
backgroundColor: '#29AAE1'
|
||||
},
|
||||
'2': {
|
||||
backgroundColor: '#7F97FA'
|
||||
},
|
||||
'3': {
|
||||
backgroundColor: '##75C5CA'
|
||||
},
|
||||
'4': {
|
||||
backgroundColor: '#A699F6'
|
||||
},
|
||||
'5': {
|
||||
backgroundColor: '#A4B5E1'
|
||||
}
|
||||
}, // 拓扑图节点分级别样式
|
||||
topoViewJsonData: {}, // 拓扑图 JSON 数据
|
||||
topoViewOption: {}, // 拓扑图配置数据 子节点分页
|
||||
topoViewSearchValue: '', // 拓扑图搜索
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
|
@ -485,6 +550,30 @@ export default {
|
|||
: {}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale': {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.changeTopoViewToolbarLang(newVal)
|
||||
},
|
||||
},
|
||||
isShowPreview: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.changeTopoViewToolbarLang(this.$i18n.locale)
|
||||
}
|
||||
},
|
||||
},
|
||||
drawerVisible: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.changeTopoViewToolbarLang(this.$i18n.locale)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeNodeTips(e) {
|
||||
e.preventDefault()
|
||||
|
@ -578,7 +667,7 @@ export default {
|
|||
payload.view_ids = g.views.map(i => i.id)
|
||||
}
|
||||
if (groupId) {
|
||||
putTopoGroupByGId(groupId, { view_ids: g.views.map((i) => i.id) })
|
||||
putTopoGroupByGId(groupId, { view_ids: payload.view_ids })
|
||||
.then(() => {
|
||||
this.$message.success(that.$t('saveSuccess'))
|
||||
})
|
||||
|
@ -729,12 +818,21 @@ export default {
|
|||
disableDefaultClickEffect: true,
|
||||
})
|
||||
})
|
||||
const type2meta = res?.type2meta
|
||||
res.nodes.forEach(item => {
|
||||
const icon = type2meta?.[item?.type_id] || ''
|
||||
nodes.push({
|
||||
id: `${item.id}`,
|
||||
text: item.name,
|
||||
nodeShape: 1,
|
||||
borderWidth: -1,
|
||||
color: 'transparent',
|
||||
styleClass: {
|
||||
padding: '0px'
|
||||
},
|
||||
data: {
|
||||
icon
|
||||
},
|
||||
disableDefaultClickEffect: true,
|
||||
})
|
||||
})
|
||||
|
@ -752,11 +850,15 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
showTopoView(viewId) {
|
||||
async showTopoView(viewId) {
|
||||
if (viewId === 'null' || !viewId) {
|
||||
return
|
||||
}
|
||||
showTopoView(viewId).then(res => {
|
||||
const topoViewRes = await getTopoView(viewId)
|
||||
if (topoViewRes?.option) {
|
||||
this.topoViewOption = topoViewRes.option
|
||||
}
|
||||
showTopoView(viewId).then(async res => {
|
||||
const nodes = []
|
||||
const links = []
|
||||
this.currentNodes = res.nodes
|
||||
|
@ -768,11 +870,21 @@ export default {
|
|||
disableDefaultClickEffect: false,
|
||||
})
|
||||
})
|
||||
const type2meta = res?.type2meta
|
||||
res.nodes.forEach(item => {
|
||||
const icon = type2meta?.[item?.type_id] || ''
|
||||
nodes.push({
|
||||
id: `${item.id}`,
|
||||
text: item.name,
|
||||
data: {},
|
||||
color: 'transparent',
|
||||
styleClass: {
|
||||
padding: '0px'
|
||||
},
|
||||
data: {
|
||||
icon
|
||||
},
|
||||
isHide: false,
|
||||
opacity: 1,
|
||||
})
|
||||
})
|
||||
const _graphJsonData = {
|
||||
|
@ -783,11 +895,132 @@ export default {
|
|||
this.$message.error(this.$t('cmdb.topo.noData'))
|
||||
return
|
||||
}
|
||||
// this.$nextTick(() => {
|
||||
this.$refs.showTopoView.setJsonData(_graphJsonData)
|
||||
// })
|
||||
this.$refs.showTopoView.setJsonData(_.cloneDeep(_graphJsonData), async () => {
|
||||
this.topoViewSearchValue = ''
|
||||
|
||||
// map 结构存储 节点
|
||||
const nodeMap = _graphJsonData.nodes.reduce((map, node) => {
|
||||
map.set(node.id, node)
|
||||
return map
|
||||
}, new Map())
|
||||
_graphJsonData.nodes = nodeMap
|
||||
|
||||
if (this?.topoViewOption?.aggregation_count) {
|
||||
const instance = this.$refs.showTopoView.getInstance()
|
||||
const nodes = instance.getNodes()
|
||||
const rootNodes = nodes.filter((node) => node.lot.level === 0)
|
||||
rootNodes.forEach((node) => {
|
||||
this.initMoreNodesData(node, _graphJsonData)
|
||||
})
|
||||
|
||||
this.$refs.showTopoView.setJsonData(_.cloneDeep({
|
||||
nodes: _graphJsonData.nodes.values(),
|
||||
links: _graphJsonData.links
|
||||
}))
|
||||
}
|
||||
this.topoViewJsonData = _graphJsonData
|
||||
this.changeTopoViewToolbarLang(this.$i18n.locale)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化子节点分页数据
|
||||
*/
|
||||
initMoreNodesData(node, jsonData) {
|
||||
const childs = node.lot.childs
|
||||
// 没有子节点 终止遍历
|
||||
if (!childs?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// 子节点分页数量
|
||||
const aggregation_count = this?.topoViewOption?.aggregation_count || 1
|
||||
// 展示节点数量
|
||||
let showNodeCount = 0
|
||||
|
||||
childs.forEach((childNode, index) => {
|
||||
if (childNode?.data?.btnType !== 'more') {
|
||||
if (showNodeCount >= aggregation_count) {
|
||||
const originNode = jsonData?.nodes?.get(childNode.id)
|
||||
if (originNode) {
|
||||
originNode.isHide = true
|
||||
}
|
||||
} else if (!childNode.isHide) {
|
||||
showNodeCount++
|
||||
}
|
||||
this.initMoreNodesData(childNode, jsonData)
|
||||
}
|
||||
})
|
||||
|
||||
if (childs.length - showNodeCount > 0) {
|
||||
const id = uuidv4()
|
||||
jsonData.nodes.set(id, {
|
||||
id,
|
||||
text: `展示更多(${childs.length - showNodeCount})`,
|
||||
data: {
|
||||
btnType: 'more'
|
||||
},
|
||||
color: 'transparent',
|
||||
styleClass: {
|
||||
padding: '0px'
|
||||
},
|
||||
})
|
||||
|
||||
jsonData.links.push({
|
||||
from: node.id,
|
||||
to: id,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async clickMoreBtn(node) {
|
||||
const childs = node?.lot?.parent?.lot?.childs
|
||||
if (childs?.length) {
|
||||
const topoViewJsonData = this.topoViewJsonData
|
||||
let moreBtnNode = null
|
||||
let showNodeCount = 0
|
||||
let toggleNodeCount = 0
|
||||
const aggregation_count = this?.topoViewOption?.aggregation_count || 1
|
||||
|
||||
childs.forEach((child) => {
|
||||
if (!child.isHide) {
|
||||
showNodeCount++
|
||||
}
|
||||
|
||||
if (toggleNodeCount < aggregation_count && child.isHide) {
|
||||
const childNode = topoViewJsonData?.nodes?.get(child.id)
|
||||
if (childNode) {
|
||||
childNode.isHide = false
|
||||
toggleNodeCount++
|
||||
showNodeCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (child.data.btnType === 'more') {
|
||||
moreBtnNode = topoViewJsonData?.nodes?.get(child.id)
|
||||
}
|
||||
})
|
||||
if (moreBtnNode) {
|
||||
if (showNodeCount === childs.length) {
|
||||
moreBtnNode.isHide = true
|
||||
} else {
|
||||
moreBtnNode.text = `展示更多(${childs.length - showNodeCount})`
|
||||
}
|
||||
}
|
||||
|
||||
const instance = this.$refs.showTopoView.getInstance()
|
||||
instance.setJsonData(
|
||||
{
|
||||
links: topoViewJsonData.links,
|
||||
nodes: topoViewJsonData.nodes.values()
|
||||
},
|
||||
false
|
||||
)
|
||||
this.topoViewJsonData = topoViewJsonData
|
||||
}
|
||||
},
|
||||
|
||||
handleOpenCmdb() {
|
||||
this.$refs.cmdbDrawer.open()
|
||||
},
|
||||
|
@ -921,9 +1154,16 @@ export default {
|
|||
}
|
||||
},
|
||||
async showNodeTips(nodeObject, $event) {
|
||||
console.log('node click')
|
||||
console.log('node click', nodeObject)
|
||||
$event.preventDefault()
|
||||
$event.stopPropagation()
|
||||
|
||||
const btnType = nodeObject?.data?.btnType
|
||||
if (btnType === 'more') {
|
||||
this.clickMoreBtn(nodeObject)
|
||||
return
|
||||
}
|
||||
|
||||
const _base_position = this.$refs.showTopoView.getInstance().options.fullscreen ? { x: 0, y: 0 } : this.$refs.rightTopoView.getBoundingClientRect()
|
||||
if (this.currentNode !== nodeObject) {
|
||||
this.currentNodeValues = null
|
||||
|
@ -934,25 +1174,16 @@ export default {
|
|||
const [ attributes ] = await Promise.all([getSubscribeAttributes(rawNode.type_id)])
|
||||
this.currentNodeAttributes = attributes?.attributes || []
|
||||
if (!this.currentNodeAttributes.length) {
|
||||
this.errorMessage = this.$t('cmdb.topo.noPreferenceAttributes')
|
||||
this.errorMessageShow = true
|
||||
this.currentNodeValues = null
|
||||
this.isShowNodeTipsPanel = false
|
||||
this.handleNullNodeTips(this.$t('cmdb.topo.noPreferenceAttributes'))
|
||||
}
|
||||
await searchCI({ q: `_id:${rawNode.id}` }, false).then(res => {
|
||||
if (!res.result.length) {
|
||||
this.errorMessage = this.$t('cmdb.topo.noInstancePerm')
|
||||
this.errorMessageShow = true
|
||||
this.currentNodeValues = null
|
||||
this.isShowNodeTipsPanel = false
|
||||
this.handleNullNodeTips(this.$t('cmdb.topo.noInstancePerm'))
|
||||
} else {
|
||||
this.currentNodeValues = res.result[0]
|
||||
}
|
||||
}).catch(error => {
|
||||
this.errorMessage = ((error.response || {}).data || {}).message
|
||||
this.errorMessageShow = true
|
||||
this.currentNodeValues = null
|
||||
this.isShowNodeTipsPanel = false
|
||||
this.handleNullNodeTips(((error.response || {}).data || {}).message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -971,9 +1202,71 @@ export default {
|
|||
this.isShowNodeTipsPanel = true
|
||||
console.log(this.nodeTipsPosition)
|
||||
},
|
||||
|
||||
handleNullNodeTips(errorMessage) {
|
||||
this.errorMessage = errorMessage
|
||||
this.errorMessageShow = true
|
||||
this.currentNodeValues = null
|
||||
this.isShowNodeTipsPanel = false
|
||||
this.currentNode = {}
|
||||
},
|
||||
|
||||
hideNodeTips(nodeObject, $event) {
|
||||
this.isShowNodeTipsPanel = false
|
||||
},
|
||||
|
||||
handleSearchTopoView(v) {
|
||||
const topoViewJsonData = this.topoViewJsonData
|
||||
topoViewJsonData.nodes.keys().forEach((key) => {
|
||||
const node = topoViewJsonData?.nodes?.get(key)
|
||||
if (node?.data?.btnType !== 'more') {
|
||||
node.opacity = node?.text?.indexOf(v) !== -1 ? 1 : 0.1
|
||||
}
|
||||
})
|
||||
const instance = this.$refs.showTopoView.getInstance()
|
||||
instance.setJsonData(
|
||||
{
|
||||
links: topoViewJsonData.links,
|
||||
nodes: topoViewJsonData.nodes.values()
|
||||
},
|
||||
false
|
||||
)
|
||||
this.topoViewJsonData = topoViewJsonData
|
||||
},
|
||||
|
||||
changeTopoViewToolbarLang(lang) {
|
||||
setTimeout(() => {
|
||||
const toolbarElements = document.getElementsByClassName('rel-toolbar')
|
||||
const zhlangMap = {
|
||||
'全屏/退出全屏': 'Full Screen/Exit Full Screen',
|
||||
'放大': 'zoom in',
|
||||
'缩小': 'zoom out',
|
||||
'刷新': 'refresh ',
|
||||
'下载图片': 'download image'
|
||||
}
|
||||
const enlangMap = {
|
||||
'Full Screen/Exit Full Screen': '全屏/退出全屏',
|
||||
'zoom in': '放大',
|
||||
'zoom out': '缩小',
|
||||
'refresh': '刷新 ',
|
||||
'download image': '下载图片'
|
||||
}
|
||||
|
||||
toolbarElements.forEach((toolbarElement) => {
|
||||
if (toolbarElement?.children?.length) {
|
||||
toolbarElement.children.forEach((node) => {
|
||||
const oldTitle = node?.getAttribute('title')
|
||||
if (oldTitle) {
|
||||
const newTitle = lang === 'en' ? zhlangMap[oldTitle] : enlangMap[oldTitle]
|
||||
if (newTitle) {
|
||||
node.setAttribute('title', newTitle)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 300)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1098,6 +1391,14 @@ export default {
|
|||
top: 40%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.relation-graph-search {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
.topo-left,
|
||||
.topo-right {
|
||||
|
@ -1133,6 +1434,54 @@ export default {
|
|||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.relation-graph-node {
|
||||
padding: 6px 3px;
|
||||
border-radius: 2px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
&-image {
|
||||
max-height: 20px;
|
||||
max-width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .relation-graph {
|
||||
background-color: #FFFFFF;
|
||||
|
||||
.rel-node {
|
||||
padding: 0px;
|
||||
height: auto !important;
|
||||
}
|
||||
.rel-node-checked {
|
||||
box-shadow: none;
|
||||
}
|
||||
.c-expanded {
|
||||
background-color: rgb(64, 158, 255) !important;
|
||||
}
|
||||
.c-collapsed {
|
||||
background-color: rgb(64, 158, 255) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
|
|
Loading…
Reference in New Issue