feat(cmdb-ui):service tree grant (#425)

This commit is contained in:
dagongren 2024-03-18 19:59:16 +08:00 committed by GitHub
parent 482d34993b
commit 42feb4b862
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 8378 additions and 7652 deletions

View File

@ -1,41 +1,41 @@
import i18n from '@/lang' import i18n from '@/lang'
export const ruleTypeList = () => { export const ruleTypeList = () => {
return [ return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') }, { value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') }, { value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' }, // { value: 'not', label: '非' },
] ]
} }
export const expList = () => { export const expList = () => {
return [ return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') }, { value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') }, { value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') }, { value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') }, { value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') }, { value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') }, { value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') }, { value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') }, { value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕 { value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') }, { value: 'value', label: i18n.t('cmdbFilterComp.value') },
] ]
} }
export const advancedExpList = () => { export const advancedExpList = () => {
return [ return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') }, { value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') }, { value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') }, { value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') }, { value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') }, { value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
] ]
} }
export const compareTypeList = [ export const compareTypeList = [
{ value: '1', label: '>' }, { value: '1', label: '>' },
{ value: '2', label: '>=' }, { value: '2', label: '>=' },
{ value: '3', label: '<' }, { value: '3', label: '<' },
{ value: '4', label: '<=' }, { value: '4', label: '<=' },
] ]

View File

@ -1,332 +1,346 @@
<template> <template>
<div> <div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id"> <a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }"> <div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect <treeselect
v-if="index" v-if="index"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }" :style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type" v-model="item.type"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="ruleTypeList" :options="ruleTypeList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
> :disabled="disabled"
</treeselect> >
</div> </treeselect>
<treeselect </div>
class="custom-treeselect" <treeselect
:style="{ width: '130px', '--custom-height': '24px' }" class="custom-treeselect"
v-model="item.property" :style="{ width: '130px', '--custom-height': '24px' }"
:multiple="false" v-model="item.property"
:clearable="false" :multiple="false"
searchable :clearable="false"
:options="canSearchPreferenceAttrList" searchable
:normalizer=" :options="canSearchPreferenceAttrList"
(node) => { :normalizer="
return { (node) => {
id: node.name, return {
label: node.alias || node.name, id: node.name,
children: node.children, label: node.alias || node.name,
} children: node.children,
} }
" }
appendToBody "
:zIndex="1050" appendToBody
> :zIndex="1050"
<div :disabled="disabled"
:title="node.label" >
slot="option-label" <div
slot-scope="{ node }" :title="node.label"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" slot="option-label"
> slot-scope="{ node }"
<ValueTypeMapIcon :attr="node.raw" /> :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
{{ node.label }} >
</div> <ValueTypeMapIcon :attr="node.raw" />
<div {{ node.label }}
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" </div>
slot="value-label" <div
slot-scope="{ node }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> slot="value-label"
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }} slot-scope="{ node }"
</div> >
</treeselect> <ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
<treeselect </div>
class="custom-treeselect" </treeselect>
:style="{ width: '100px', '--custom-height': '24px' }" <treeselect
v-model="item.exp" class="custom-treeselect"
:multiple="false" :style="{ width: '100px', '--custom-height': '24px' }"
:clearable="false" v-model="item.exp"
searchable :multiple="false"
:options="[...getExpListByProperty(item.property), ...advancedExpList]" :clearable="false"
:normalizer=" searchable
(node) => { :options="[...getExpListByProperty(item.property), ...advancedExpList]"
return { :normalizer="
id: node.value, (node) => {
label: node.label, return {
children: node.children, id: node.value,
} label: node.label,
} children: node.children,
" }
@select="(value) => handleChangeExp(value, item, index)" }
appendToBody "
:zIndex="1050" @select="(value) => handleChangeExp(value, item, index)"
> appendToBody
</treeselect> :zIndex="1050"
<treeselect :disabled="disabled"
class="custom-treeselect" >
:style="{ width: '175px', '--custom-height': '24px' }" </treeselect>
v-model="item.value" <treeselect
:multiple="false" class="custom-treeselect"
:clearable="false" :style="{ width: '175px', '--custom-height': '24px' }"
searchable v-model="item.value"
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" :multiple="false"
:options="getChoiceValueByProperty(item.property)" :clearable="false"
:placeholder="$t('placeholder2')" searchable
:normalizer=" v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
(node) => { :options="getChoiceValueByProperty(item.property)"
return { :placeholder="$t('placeholder2')"
id: node[0], :normalizer="
label: node[0], (node) => {
children: node.children, return {
} id: node[0],
} label: node[0],
" children: node.children,
appendToBody }
:zIndex="1050" }
> "
<div appendToBody
:title="node.label" :zIndex="1050"
slot="option-label" :disabled="disabled"
slot-scope="{ node }" >
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" <div
> :title="node.label"
{{ node.label }} slot="option-label"
</div> slot-scope="{ node }"
</treeselect> :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
<a-input-group >
size="small" {{ node.label }}
compact </div>
v-else-if="item.exp === 'range' || item.exp === '~range'" </treeselect>
:style="{ width: '175px' }" <a-input-group
> size="small"
<a-input compact
class="ops-input" v-else-if="item.exp === 'range' || item.exp === '~range'"
size="small" :style="{ width: '175px' }"
v-model="item.min" >
:style="{ width: '78px' }" <a-input
:placeholder="$t('min')" class="ops-input"
/> size="small"
~ v-model="item.min"
<a-input :style="{ width: '78px' }"
class="ops-input" :placeholder="$t('min')"
size="small" :disabled="disabled"
v-model="item.max" />
:style="{ width: '78px' }" ~
:placeholder="$t('max')" <a-input
/> class="ops-input"
</a-input-group> size="small"
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> v-model="item.max"
<treeselect :style="{ width: '78px' }"
class="custom-treeselect" :placeholder="$t('max')"
:style="{ width: '60px', '--custom-height': '24px' }" :disabled="disabled"
v-model="item.compareType" />
:multiple="false" </a-input-group>
:clearable="false" <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
searchable <treeselect
:options="compareTypeList" class="custom-treeselect"
:normalizer=" :style="{ width: '60px', '--custom-height': '24px' }"
(node) => { v-model="item.compareType"
return { :multiple="false"
id: node.value, :clearable="false"
label: node.label, searchable
children: node.children, :options="compareTypeList"
} :normalizer="
} (node) => {
" return {
appendToBody id: node.value,
:zIndex="1050" label: node.label,
> children: node.children,
</treeselect> }
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> }
</a-input-group> "
<a-input appendToBody
v-else-if="item.exp !== 'value' && item.exp !== '~value'" :zIndex="1050"
size="small" :disabled="disabled"
v-model="item.value" >
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" </treeselect>
class="ops-input" <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
:style="{ width: '175px' }" </a-input-group>
></a-input> <a-input
<div v-else :style="{ width: '175px' }"></div> v-else-if="item.exp !== 'value' && item.exp !== '~value'"
<a-tooltip :title="$t('copy')"> size="small"
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> v-model="item.value"
</a-tooltip> :placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
<a-tooltip :title="$t('delete')"> class="ops-input"
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> :style="{ width: '175px' }"
</a-tooltip> :disabled="disabled"
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> ></a-input>
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> <div v-else :style="{ width: '175px' }"></div>
</a-tooltip> <template v-if="!disabled">
</a-space> <a-tooltip :title="$t('copy')">
<div class="table-filter-add"> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
<a @click="handleAddRule">+ {{ $t('new') }}</a> </a-tooltip>
</div> <a-tooltip :title="$t('delete')">
</div> <a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</template> </a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<script> <a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
import _ from 'lodash' </a-tooltip>
import { v4 as uuidv4 } from 'uuid' </template>
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' </a-space>
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' <div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
export default { </div>
name: 'Expression', </div>
components: { ValueTypeMapIcon }, </template>
model: {
prop: 'value', <script>
event: 'change', import _ from 'lodash'
}, import { v4 as uuidv4 } from 'uuid'
props: { import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
value: { import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
type: Array,
default: () => [], export default {
}, name: 'Expression',
canSearchPreferenceAttrList: { components: { ValueTypeMapIcon },
type: Array, model: {
required: true, prop: 'value',
default: () => [], event: 'change',
}, },
needAddHere: { props: {
type: Boolean, value: {
default: false, type: Array,
}, default: () => [],
}, },
data() { canSearchPreferenceAttrList: {
return { type: Array,
compareTypeList, required: true,
} default: () => [],
}, },
computed: { needAddHere: {
ruleList: { type: Boolean,
get() { default: false,
return this.value },
}, disabled: {
set(val) { type: Boolean,
this.$emit('change', val) default: false,
return val },
}, },
}, data() {
ruleTypeList() { return {
return ruleTypeList() compareTypeList,
}, }
expList() { },
return expList() computed: {
}, ruleList: {
advancedExpList() { get() {
return advancedExpList() return this.value
}, },
}, set(val) {
methods: { this.$emit('change', val)
getExpListByProperty(property) { return val
if (property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) },
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { ruleTypeList() {
return [ return ruleTypeList()
{ value: 'is', label: this.$t('cmdbFilterComp.is') }, },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') }, expList() {
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕 return expList()
{ value: 'value', label: this.$t('cmdbFilterComp.value') }, },
] advancedExpList() {
} return advancedExpList()
return this.expList },
} },
return this.expList methods: {
}, getExpListByProperty(property) {
isChoiceByProperty(property) { if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) { if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return _find.is_choice return [
} { value: 'is', label: this.$t('cmdbFilterComp.is') },
return false { value: '~is', label: this.$t('cmdbFilterComp.~is') },
}, { value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
handleAddRule() { { value: 'value', label: this.$t('cmdbFilterComp.value') },
this.ruleList.push({ ]
id: uuidv4(), }
type: 'and', return this.expList
property: this.canSearchPreferenceAttrList[0]?.name, }
exp: 'is', return this.expList
value: null, },
}) isChoiceByProperty(property) {
this.$emit('change', this.ruleList) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
}, if (_find) {
handleCopyRule(item) { return _find.is_choice
this.ruleList.push({ ...item, id: uuidv4() }) }
this.$emit('change', this.ruleList) return false
}, },
handleDeleteRule(item) { handleAddRule() {
const idx = this.ruleList.findIndex((r) => r.id === item.id) this.ruleList.push({
if (idx > -1) { id: uuidv4(),
this.ruleList.splice(idx, 1) type: 'and',
} property: this.canSearchPreferenceAttrList[0]?.name,
this.$emit('change', this.ruleList) exp: 'is',
}, value: null,
handleAddRuleAt(item) { })
const idx = this.ruleList.findIndex((r) => r.id === item.id) this.$emit('change', this.ruleList)
if (idx > -1) { },
this.ruleList.splice(idx, 0, { handleCopyRule(item) {
id: uuidv4(), this.ruleList.push({ ...item, id: uuidv4() })
type: 'and', this.$emit('change', this.ruleList)
property: this.canSearchPreferenceAttrList[0]?.name, },
exp: 'is', handleDeleteRule(item) {
value: null, const idx = this.ruleList.findIndex((r) => r.id === item.id)
}) if (idx > -1) {
} this.ruleList.splice(idx, 1)
this.$emit('change', this.ruleList) }
}, this.$emit('change', this.ruleList)
getChoiceValueByProperty(property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) handleAddRuleAt(item) {
if (_find) { const idx = this.ruleList.findIndex((r) => r.id === item.id)
return _find.choice_value if (idx > -1) {
} this.ruleList.splice(idx, 0, {
return [] id: uuidv4(),
}, type: 'and',
handleChangeExp({ value }, item, index) { property: this.canSearchPreferenceAttrList[0]?.name,
const _ruleList = _.cloneDeep(this.ruleList) exp: 'is',
if (value === 'range') { value: null,
_ruleList[index] = { })
..._ruleList[index], }
min: '', this.$emit('change', this.ruleList)
max: '', },
exp: value, getChoiceValueByProperty(property) {
} const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
} else if (value === 'compare') { if (_find) {
_ruleList[index] = { return _find.choice_value
..._ruleList[index], }
compareType: '1', return []
exp: value, },
} handleChangeExp({ value }, item, index) {
} else { const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = { if (value === 'range') {
..._ruleList[index], _ruleList[index] = {
exp: value, ..._ruleList[index],
} min: '',
} max: '',
this.ruleList = _ruleList exp: value,
this.$emit('change', this.ruleList) }
}, } else if (value === 'compare') {
}, _ruleList[index] = {
} ..._ruleList[index],
</script> compareType: '1',
exp: value,
<style></style> }
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@ -1,296 +1,302 @@
<template> <template>
<div> <div>
<a-popover <a-popover
v-if="isDropdown" v-if="isDropdown"
v-model="visible" v-model="visible"
trigger="click" trigger="click"
:placement="placement" :placement="placement"
overlayClassName="table-filter" overlayClassName="table-filter"
@visibleChange="visibleChange" @visibleChange="visibleChange"
> >
<slot name="popover_item"> <slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button> <a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot> </slot>
<template slot="content"> <template slot="content">
<Expression <Expression
:needAddHere="needAddHere" :needAddHere="needAddHere"
v-model="ruleList" v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/> :disabled="disabled"
<a-divider :style="{ margin: '10px 0' }" /> />
<div style="width:554px"> <a-divider :style="{ margin: '10px 0' }" />
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }"> <div style="width:554px">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button> <a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button> <a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
</a-space> <a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</div> </a-space>
</template> </div>
</a-popover> </template>
<Expression </a-popover>
:needAddHere="needAddHere" <Expression
v-else :needAddHere="needAddHere"
v-model="ruleList" v-else
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" v-model="ruleList"
/> :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
</div> :disabled="disabled"
</template> />
</div>
<script> </template>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue' <script>
import { advancedExpList, compareTypeList } from './constants' import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
export default { import { advancedExpList, compareTypeList } from './constants'
name: 'FilterComp',
components: { Expression }, export default {
props: { name: 'FilterComp',
canSearchPreferenceAttrList: { components: { Expression },
type: Array, props: {
required: true, canSearchPreferenceAttrList: {
default: () => [], type: Array,
}, required: true,
expression: { default: () => [],
type: String, },
default: '', expression: {
}, type: String,
regQ: { default: '',
type: String, },
default: '(?<=q=).+(?=&)|(?<=q=).+$', regQ: {
}, type: String,
placement: { default: '(?<=q=).+(?=&)|(?<=q=).+$',
type: String, },
default: 'bottomRight', placement: {
}, type: String,
isDropdown: { default: 'bottomRight',
type: Boolean, },
default: true, isDropdown: {
}, type: Boolean,
needAddHere: { default: true,
type: Boolean, },
default: false, needAddHere: {
}, type: Boolean,
}, default: false,
data() { },
return { disabled: {
advancedExpList, type: Boolean,
compareTypeList, default: false,
visible: false, },
ruleList: [], },
filterExp: '', data() {
} return {
}, advancedExpList,
compareTypeList,
methods: { visible: false,
visibleChange(open, isInitOne = true) { ruleList: [],
// isInitOne 初始化exp为空时ruleList是否默认给一条 filterExp: '',
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g }
const exp = this.expression.match(new RegExp(this.regQ, 'g')) },
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null methods: {
if (open && exp) { visibleChange(open, isInitOne = true) {
const expArray = exp.split(',').map((item) => { // isInitOne 初始化exp为空时ruleList是否默认给一条
let has_not = '' // const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const key = item.split(':')[0] const exp = this.expression.match(new RegExp(this.regQ, 'g'))
const val = item ? this.expression.match(new RegExp(this.regQ, 'g'))[0]
.split(':') : null
.slice(1) if (open && exp) {
.join(':') const expArray = exp.split(',').map((item) => {
let type, property, exp, value, min, max, compareType let has_not = ''
if (key.includes('-')) { const key = item.split(':')[0]
type = 'or' const val = item
if (key.includes('~')) { .split(':')
property = key.substring(2) .slice(1)
has_not = '~' .join(':')
} else { let type, property, exp, value, min, max, compareType
property = key.substring(1) if (key.includes('-')) {
} type = 'or'
} else { if (key.includes('~')) {
type = 'and' property = key.substring(2)
if (key.includes('~')) { has_not = '~'
property = key.substring(1) } else {
has_not = '~' property = key.substring(1)
} else { }
property = key } else {
} type = 'and'
} if (key.includes('~')) {
property = key.substring(1)
const in_reg = /(?<=\().+(?=\))/g has_not = '~'
const range_reg = /(?<=\[).+(?=\])/g } else {
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/ property = key
if (val === '*') { }
exp = has_not + 'value' }
value = ''
} else if (in_reg.test(val)) { const in_reg = /(?<=\().+(?=\))/g
exp = has_not + 'in' const range_reg = /(?<=\[).+(?=\])/g
value = val.match(in_reg)[0] const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
} else if (range_reg.test(val)) { if (val === '*') {
exp = has_not + 'range' exp = has_not + 'value'
value = val.match(range_reg)[0] value = ''
min = value.split('_TO_')[0] } else if (in_reg.test(val)) {
max = value.split('_TO_')[1] exp = has_not + 'in'
} else if (compare_reg.test(val)) { value = val.match(in_reg)[0]
exp = has_not + 'compare' } else if (range_reg.test(val)) {
value = val.match(compare_reg)[0] exp = has_not + 'range'
const _compareType = val.substring(0, val.match(compare_reg)['index']) value = val.match(range_reg)[0]
const idx = compareTypeList.findIndex((item) => item.label === _compareType) min = value.split('_TO_')[0]
compareType = compareTypeList[idx].value max = value.split('_TO_')[1]
} else if (!val.includes('*')) { } else if (compare_reg.test(val)) {
exp = has_not + 'is' exp = has_not + 'compare'
value = val value = val.match(compare_reg)[0]
} else { const _compareType = val.substring(0, val.match(compare_reg)['index'])
const resList = [ const idx = compareTypeList.findIndex((item) => item.label === _compareType)
['contain', /(?<=\*).*(?=\*)/g], compareType = compareTypeList[idx].value
['end_with', /(?<=\*).+/g], } else if (!val.includes('*')) {
['start_with', /.+(?=\*)/g], exp = has_not + 'is'
] value = val
for (let i = 0; i < 3; i++) { } else {
const reg = resList[i] const resList = [
if (reg[1].test(val)) { ['contain', /(?<=\*).*(?=\*)/g],
exp = has_not + reg[0] ['end_with', /(?<=\*).+/g],
value = val.match(reg[1])[0] ['start_with', /.+(?=\*)/g],
break ]
} for (let i = 0; i < 3; i++) {
} const reg = resList[i]
} if (reg[1].test(val)) {
return { exp = has_not + reg[0]
id: uuidv4(), value = val.match(reg[1])[0]
type, break
property, }
exp, }
value, }
min, return {
max, id: uuidv4(),
compareType, type,
} property,
}) exp,
this.ruleList = [...expArray] value,
} else if (open) { min,
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password) max,
this.ruleList = isInitOne compareType,
? [ }
{ })
id: uuidv4(), this.ruleList = [...expArray]
type: 'and', } else if (open) {
property: const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length this.ruleList = isInitOne
? _canSearchPreferenceAttrList[0].name ? [
: undefined, {
exp: 'is', id: uuidv4(),
value: null, type: 'and',
}, property:
] _canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
: [] ? _canSearchPreferenceAttrList[0].name
} : undefined,
}, exp: 'is',
handleClear() { value: null,
this.ruleList = [ },
{ ]
id: uuidv4(), : []
type: 'and', }
property: this.canSearchPreferenceAttrList[0].name, },
exp: 'is', handleClear() {
value: null, this.ruleList = [
}, {
] id: uuidv4(),
this.filterExp = '' type: 'and',
this.visible = false property: this.canSearchPreferenceAttrList[0].name,
this.$emit('setExpFromFilter', this.filterExp) exp: 'is',
}, value: null,
handleSubmit() { },
if (this.ruleList && this.ruleList.length) { ]
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and this.filterExp = ''
this.filterExp = '' this.visible = false
const expList = this.ruleList.map((rule) => { this.$emit('setExpFromFilter', this.filterExp)
let singleRuleExp = '' },
let _exp = rule.exp handleSubmit() {
if (rule.type === 'or') { if (this.ruleList && this.ruleList.length) {
singleRuleExp += '-' this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
} this.filterExp = ''
if (rule.exp.includes('~')) { const expList = this.ruleList.map((rule) => {
singleRuleExp += '~' let singleRuleExp = ''
_exp = rule.exp.split('~')[1] let _exp = rule.exp
} if (rule.type === 'or') {
singleRuleExp += `${rule.property}:` singleRuleExp += '-'
if (_exp === 'is') { }
singleRuleExp += `${rule.value ?? ''}` if (rule.exp.includes('~')) {
} singleRuleExp += '~'
if (_exp === 'contain') { _exp = rule.exp.split('~')[1]
singleRuleExp += `*${rule.value ?? ''}*` }
} singleRuleExp += `${rule.property}:`
if (_exp === 'start_with') { if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}*` singleRuleExp += `${rule.value ?? ''}`
} }
if (_exp === 'end_with') { if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}` singleRuleExp += `*${rule.value ?? ''}*`
} }
if (_exp === 'value') { if (_exp === 'start_with') {
singleRuleExp += `*` singleRuleExp += `${rule.value ?? ''}*`
} }
if (_exp === 'in') { if (_exp === 'end_with') {
singleRuleExp += `(${rule.value ?? ''})` singleRuleExp += `*${rule.value ?? ''}`
} }
if (_exp === 'range') { if (_exp === 'value') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]` singleRuleExp += `*`
} }
if (_exp === 'compare') { if (_exp === 'in') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType) singleRuleExp += `(${rule.value ?? ''})`
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}` }
} if (_exp === 'range') {
return singleRuleExp singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}) }
this.filterExp = expList.join(',') if (_exp === 'compare') {
this.$emit('setExpFromFilter', this.filterExp) const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
} else { singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
this.$emit('setExpFromFilter', '') }
} return singleRuleExp
this.visible = false })
}, this.filterExp = expList.join(',')
}, this.$emit('setExpFromFilter', this.filterExp)
} } else {
</script> this.$emit('setExpFromFilter', '')
}
<style lang="less" scoped> this.visible = false
.table-filter { },
.table-filter-add { },
margin-top: 10px; }
& > a { </script>
padding: 2px 8px;
&:hover { <style lang="less" scoped>
background-color: #f0faff; .table-filter {
border-radius: 5px; .table-filter-add {
} margin-top: 10px;
} & > a {
} padding: 2px 8px;
.table-filter-extra-icon { &:hover {
padding: 0px 2px; background-color: #f0faff;
&:hover { border-radius: 5px;
display: inline-block; }
border-radius: 5px; }
background-color: #f0faff; }
} .table-filter-extra-icon {
} padding: 0px 2px;
} &:hover {
</style> display: inline-block;
border-radius: 5px;
<style lang="less"> background-color: #f0faff;
.table-filter-extra-operation { }
.ant-popover-inner-content { }
padding: 3px 4px; }
.operation { </style>
cursor: pointer;
width: 90px; <style lang="less">
height: 30px; .table-filter-extra-operation {
line-height: 30px; .ant-popover-inner-content {
padding: 3px 4px; padding: 3px 4px;
border-radius: 5px; .operation {
transition: all 0.3s; cursor: pointer;
&:hover { width: 90px;
background-color: #f0faff; height: 30px;
} line-height: 30px;
> .anticon { padding: 3px 4px;
margin-right: 10px; border-radius: 5px;
} transition: all 0.3s;
} &:hover {
} background-color: #f0faff;
} }
</style> > .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@ -1,179 +1,183 @@
<template> <template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }"> <div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1"> <div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot> <slot name="one"></slot>
</div> </div>
<div class="spliter-wrap"> <div class="spliter-wrap">
<a-button <a-button
v-show="collapsable" v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'" :icon="isExpanded ? 'left' : 'right'"
class="collapse-btn" class="collapse-btn"
@click="handleExpand" @click="handleExpand"
></a-button> ></a-button>
<div <div
class="pane-trigger" class="pane-trigger"
@mousedown="handleMouseDown" @mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }" :style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div> ></div>
</div> </div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2"> <div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot> <slot name="two"></slot>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'SplitPane', name: 'SplitPane',
props: { props: {
direction: { direction: {
type: String, type: String,
default: 'row', default: 'row',
}, },
min: { min: {
type: Number, type: Number,
default: 10, default: 10,
}, },
max: { max: {
type: Number, type: Number,
default: 90, default: 90,
}, },
paneLengthPixel: { paneLengthPixel: {
type: Number, type: Number,
default: 220, default: 220,
}, },
triggerLength: { triggerLength: {
type: Number, type: Number,
default: 8, default: 8,
}, },
appName: { appName: {
type: String, type: String,
default: 'viewer', default: 'viewer',
}, },
collapsable: { collapsable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
triggerColor: { triggerColor: {
type: String, type: String,
default: '#f0f2f5', default: '#f0f2f5',
}, },
}, },
data() { data() {
return { return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量 triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`) isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`)) ? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false, : false,
parentContainer: null, parentContainer: null,
} }
}, },
computed: { computed: {
lengthType() { lengthType() {
return this.direction === 'row' ? 'width' : 'height' return this.direction === 'row' ? 'width' : 'height'
}, },
minLengthType() { minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight' return this.direction === 'row' ? 'minWidth' : 'minHeight'
}, },
paneLengthValue1() { paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})` return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
}, },
paneLengthValue2() { paneLengthValue2() {
const rest = 100 - this.paneLengthPercent const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})` return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
}, },
paneLengthPercent() { paneLengthPercent() {
const clientRectWidth = this.parentContainer const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth ? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width : document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100 return (this.paneLengthPixel / clientRectWidth) * 100
}, },
}, },
watch: { watch: {
isExpanded(newValue) { isExpanded(newValue) {
if (newValue) { if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else { } else {
document.querySelector(`.${this.appName} .pane-two`).style.display = '' document.querySelector(`.${this.appName} .pane-two`).style.display = ''
} }
}, },
}, },
mounted() { mounted() {
this.parentContainer = document.querySelector(`.${this.appName}`) const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (this.isExpanded) { if (paneLengthPixel) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
} else { }
document.querySelector(`.${this.appName} .pane-two`).style.display = '' this.parentContainer = document.querySelector(`.${this.appName}`)
} if (this.isExpanded) {
}, document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
methods: { document.querySelector(`.${this.appName} .pane-two`).style.display = ''
// 按下滑动器 }
handleMouseDown(e) { },
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp) methods: {
if (this.direction === 'row') { // 按下滑动器
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left handleMouseDown(e) {
} else { document.addEventListener('mousemove', this.handleMouseMove)
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top document.addEventListener('mouseup', this.handleMouseUp)
} if (this.direction === 'row') {
}, this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
// 按下滑动器后移动鼠标 this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
handleMouseMove(e) { }
this.isExpanded = false },
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect() // 按下滑动器后移动鼠标
let paneLengthPixel = 0 handleMouseMove(e) {
this.isExpanded = false
if (this.direction === 'row') { this.$emit('expand', this.isExpanded)
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 const clientRect = this.$refs.splitPane.getBoundingClientRect()
paneLengthPixel = offset let paneLengthPixel = 0
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 if (this.direction === 'row') {
paneLengthPixel = offset const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
} paneLengthPixel = offset
} else {
if (paneLengthPixel < this.min) { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = this.min paneLengthPixel = offset
} }
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max if (paneLengthPixel < this.min) {
} paneLengthPixel = this.min
}
this.$emit('update:paneLengthPixel', paneLengthPixel) if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel) }
},
this.$emit('update:paneLengthPixel', paneLengthPixel)
// 松开滑动器
handleMouseUp() { localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
document.removeEventListener('mousemove', this.handleMouseMove) },
},
// 松开滑动器
handleExpand() { handleMouseUp() {
this.isExpanded = !this.isExpanded document.removeEventListener('mousemove', this.handleMouseMove)
this.$emit('expand', this.isExpanded) },
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
}, handleExpand() {
}, this.isExpanded = !this.isExpanded
} this.$emit('expand', this.isExpanded)
</script> localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
<style scoped lang="less"> },
@import './index.less'; }
</style> </script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@ -1,2 +1,2 @@
import SplitPane from './SplitPane' import SplitPane from './SplitPane'
export default SplitPane export default SplitPane

View File

@ -1,48 +1,48 @@
.split-pane { .split-pane {
height: 100%; height: 100%;
display: flex; display: flex;
} }
.split-pane .pane-two { .split-pane .pane-two {
flex: 1; flex: 1;
} }
.split-pane .pane-trigger { .split-pane .pane-trigger {
user-select: none; user-select: none;
} }
.split-pane.row .pane-one { .split-pane.row .pane-one {
width: 20%; width: 20%;
height: 100%; height: 100%;
// overflow-y: auto; // overflow-y: auto;
} }
.split-pane.column .pane { .split-pane.column .pane {
width: 100%; width: 100%;
} }
.split-pane.row .pane-trigger { .split-pane.row .pane-trigger {
width: 8px; width: 8px;
height: 100%; height: 100%;
cursor: e-resize; cursor: e-resize;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=') background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=')
1px 50% no-repeat #f0f2f5; 1px 50% no-repeat #f0f2f5;
} }
.split-pane .collapse-btn { .split-pane .collapse-btn {
width: 25px; width: 25px;
height: 70px; height: 70px;
position: absolute; position: absolute;
right: 8px; right: 8px;
top: calc(50% - 35px); top: calc(50% - 35px);
background-color: #f0f2f5; background-color: #f0f2f5;
border-color: transparent; border-color: transparent;
border-radius: 8px 0px 0px 8px; border-radius: 8px 0px 0px 8px;
.anticon { .anticon {
color: #7cb0fe; color: #7cb0fe;
} }
} }
.split-pane .spliter-wrap { .split-pane .spliter-wrap {
position: relative; position: relative;
} }

View File

@ -25,6 +25,7 @@ export default {
deleting: 'Deleting', deleting: 'Deleting',
deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed', deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed',
grant: 'Grant', grant: 'Grant',
revoke: 'Revoke',
login_at: 'Login At', login_at: 'Login At',
logout_at: 'Logout At', logout_at: 'Logout At',
createSuccess: 'Create Success', createSuccess: 'Create Success',

View File

@ -25,6 +25,7 @@ export default {
deleting: '正在删除', deleting: '正在删除',
deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个', deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
grant: '授权', grant: '授权',
revoke: '回收',
login_at: '登录时间', login_at: '登录时间',
logout_at: '登出时间', logout_at: '登出时间',
createSuccess: '创建成功', createSuccess: '创建成功',

View File

@ -223,3 +223,10 @@ export function deleteCiTypeInheritance(data) {
data data
}) })
} }
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@ -1,150 +1,150 @@
<template> <template>
<div class="ci-type-grant"> <div class="ci-type-grant">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
size="mini" size="mini"
stripe stripe
class="ops-stripe-table" class="ops-stripe-table"
:data="filterTableData" :data="filterTableData"
:max-height="`${tableHeight}px`" :max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)" :row-style="(params) => getCurrentRowStyle(params, addedRids)"
> >
<vxe-column field="name"></vxe-column> <vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]"> <vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}"> <template #default="{row}">
<ReadCheckbox <ReadCheckbox
v-if="['read'].includes(col.split('_')[0])" v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]" :value="row[col.split('_')[0]]"
:valueKey="col" :valueKey="col"
:rid="row.rid" :rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)" @openReadGrantModal="() => openReadGrantModal(col, row)"
/> />
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox> <a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox> <a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template> </template>
</vxe-column> </vxe-column>
<template #empty> <template #empty>
<div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB"> <div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB">
<a-icon type="loading" /> {{ $t('loading') }} <a-icon type="loading" /> {{ $t('loading') }}
</div> </div>
<div v-else> <div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" /> <img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div> <div>{{ $t('noData') }}</div>
</div> </div>
</template> </template>
</vxe-table> </vxe-table>
<a-space> <a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span> <span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span> <span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space> </a-space>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import { permMap } from './constants.js' import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType' import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue' import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowStyle } from './utils' import { getCurrentRowStyle } from './utils'
export default { export default {
name: 'CiTypeGrant', name: 'CiTypeGrant',
components: { ReadCheckbox }, components: { ReadCheckbox },
inject: ['loading', 'isModal'], inject: ['loading', 'isModal'],
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
default: null, default: null,
}, },
tableData: { tableData: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
grantType: { grantType: {
type: String, type: String,
default: 'ci_type', default: 'ci_type',
}, },
addedRids: { addedRids: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
computed: { computed: {
filterTableData() { filterTableData() {
const _tableData = this.tableData.filter((data) => { const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection( const _intersection = _.intersection(
Object.keys(data), Object.keys(data),
this.columns.map((col) => col.split('_')[0]) this.columns.map((col) => col.split('_')[0])
) )
return _intersection && _intersection.length return _intersection && _intersection.length
}) })
return _.uniqBy(_tableData, (item) => item.rid) return _.uniqBy(_tableData, (item) => item.rid)
}, },
columns() { columns() {
if (this.grantType === 'ci_type') { if (this.grantType === 'ci_type') {
return ['config', 'grant'] return ['config', 'grant']
} }
return ['read_attr', 'read_ci', 'create', 'update', 'delete'] return ['read_attr', 'read_ci', 'create', 'update', 'delete']
}, },
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
tableHeight() { tableHeight() {
if (this.isModal) { if (this.isModal) {
return (this.windowHeight - 104) / 2 return (this.windowHeight - 104) / 2
} }
return (this.windowHeight - 104) / 2 - 116 return (this.windowHeight - 104) / 2 - 116
}, },
permMap() { permMap() {
return permMap() return permMap()
} }
}, },
methods: { methods: {
getCurrentRowStyle, getCurrentRowStyle,
async handleChange(e, col, row) { async handleChange(e, col, row) {
if (e.target.checked) { if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => { await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} else { } else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => { await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} }
}, },
grantDepart() { grantDepart() {
this.$emit('grantDepart', this.grantType) this.$emit('grantDepart', this.grantType)
}, },
grantRole() { grantRole() {
this.$emit('grantRole', this.grantType) this.$emit('grantRole', this.grantType)
}, },
openReadGrantModal(col, row) { openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row) this.$emit('openReadGrantModal', col, row)
}, },
clickGrant(col, row, rowIndex) { clickGrant(col, row, rowIndex) {
if (!row[col]) { if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row) this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid) const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true }) this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else { } else {
const that = this const that = this
this.$confirm({ this.$confirm({
title: that.$t('warning'), title: that.$t('warning'),
content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }), content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }),
onOk() { onOk() {
that.handleChange({ target: { checked: false } }, col, row) that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid) const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false }) that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
}, },
}) })
} }
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.ci-type-grant { .ci-type-grant {
padding: 10px 0; padding: 10px 0;
} }
</style> </style>

View File

@ -1,15 +1,15 @@
import i18n from '@/lang' import i18n from '@/lang'
export const permMap = () => { export const permMap = () => {
return { return {
read: i18n.t('view'), read: i18n.t('view'),
add: i18n.t('new'), add: i18n.t('new'),
create: i18n.t('new'), create: i18n.t('new'),
update: i18n.t('update'), update: i18n.t('update'),
delete: i18n.t('delete'), delete: i18n.t('delete'),
config: i18n.t('cmdb.components.config'), config: i18n.t('cmdb.components.config'),
grant: i18n.t('grant'), grant: i18n.t('grant'),
'read_attr': i18n.t('cmdb.components.readAttribute'), 'read_attr': i18n.t('cmdb.components.readAttribute'),
'read_ci': i18n.t('cmdb.components.readCI') 'read_ci': i18n.t('cmdb.components.readCI')
} }
} }

View File

@ -1,343 +1,343 @@
<template> <template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }"> <div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }">
<template v-if="cmdbGrantType.includes('ci_type')"> <template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant <CiTypeGrant
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci_type" grantType="ci_type"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_ci_type" ref="grant_ci_type"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template <template
v-if=" v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type')) cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
" "
> >
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant <CiTypeGrant
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci" grantType="ci"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
@openReadGrantModal="openReadGrantModal" @openReadGrantModal="openReadGrantModal"
ref="grant_ci" ref="grant_ci"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('type_relation')"> <template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant <TypeRelationGrant
:typeRelationIds="typeRelationIds" :typeRelationIds="typeRelationIds"
:tableData="tableData" :tableData="tableData"
grantType="type_relation" grantType="type_relation"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_type_relation" ref="grant_type_relation"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('relation_view')"> <template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div> <div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant <RelationViewGrant
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:tableData="tableData" :tableData="tableData"
grantType="relation_view" grantType="relation_view"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_relation_view" ref="grant_relation_view"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<GrantModal ref="grantModal" @handleOk="handleOk" /> <GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" /> <ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue' import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue' import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource' import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission' import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue' import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal' import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue' import RelationViewGrant from './relationViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType' import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default { export default {
name: 'GrantComp', name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal }, components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
default: null, default: null,
}, },
resourceTypeName: { resourceTypeName: {
type: String, type: String,
default: '', default: '',
}, },
resourceType: { resourceType: {
type: String, type: String,
default: 'CIType', default: 'CIType',
}, },
app_id: { app_id: {
type: String, type: String,
default: 'cmdb', default: 'cmdb',
}, },
cmdbGrantType: { cmdbGrantType: {
type: String, type: String,
default: 'ci_type,ci', default: 'ci_type,ci',
}, },
typeRelationIds: { typeRelationIds: {
type: Array, type: Array,
default: null, default: null,
}, },
isModal: { isModal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
inject: ['resource_type'], inject: ['resource_type'],
data() { data() {
return { return {
tableData: [], tableData: [],
grantType: '', grantType: '',
resource_id: null, resource_id: null,
attrGroup: [], attrGroup: [],
filerPerimissions: {}, filerPerimissions: {},
loading: false, loading: false,
addedRids: [], // added rid this time addedRids: [], // added rid this time
} }
}, },
computed: { computed: {
...mapState({ ...mapState({
allEmployees: (state) => state.user.allEmployees, allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments, allDepartments: (state) => state.user.allDepartments,
}), }),
child_resource_type() { child_resource_type() {
return this.resource_type() return this.resource_type()
}, },
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
}, },
provide() { provide() {
return { return {
attrGroup: () => { attrGroup: () => {
return this.attrGroup return this.attrGroup
}, },
filerPerimissions: () => { filerPerimissions: () => {
return this.filerPerimissions return this.filerPerimissions
}, },
loading: () => { loading: () => {
return this.loading return this.loading
}, },
isModal: this.isModal, isModal: this.isModal,
} }
}, },
watch: { watch: {
resourceTypeName: { resourceTypeName: {
immediate: true, immediate: true,
handler() { handler() {
this.init() this.init()
}, },
}, },
CITypeId: { CITypeId: {
immediate: true, immediate: true,
handler() { handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) { if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions() this.getFilterPermissions()
this.getAttrGroup() this.getAttrGroup()
} }
}, },
}, },
}, },
mounted() {}, mounted() {},
methods: { methods: {
getAttrGroup() { getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => { getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res this.attrGroup = res
}) })
}, },
getFilterPermissions() { getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => { ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res this.filerPerimissions = res
}) })
}, },
async init() { async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType) const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0 const resource_type_id = _find?.id ?? 0
const res = await searchResource({ const res = await searchResource({
app_id: this.app_id, app_id: this.app_id,
resource_type_id, resource_type_id,
page_size: 9999, page_size: 9999,
}) })
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName) const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName) console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0 this.resource_id = _tempFind?.id || 0
this.getTableData() this.getTableData()
}, },
async getTableData() { async getTableData() {
this.loading = true this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 }) const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = [] const perms = []
for (const key in _tableData) { for (const key in _tableData) {
const obj = {} const obj = {}
obj.name = key obj.name = key
_tableData[key].perms.forEach((perm) => { _tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null obj.rid = perm?.rid ?? null
}) })
perms.push(obj) perms.push(obj)
} }
this.tableData = perms this.tableData = perms
this.loading = false this.loading = false
}, },
// Grant the department in common-setting and get the roleid from it // Grant the department in common-setting and get the roleid from it
grantDepart(grantType) { grantDepart(grantType) {
this.$refs.grantModal.open('depart') this.$refs.grantModal.open('depart')
this.grantType = grantType this.grantType = grantType
}, },
// Grant the oldest role permissions // Grant the oldest role permissions
grantRole(grantType) { grantRole(grantType) {
this.$refs.grantModal.open('role') this.$refs.grantModal.open('role')
this.grantType = grantType this.grantType = grantType
}, },
handleOk(params, type) { handleOk(params, type) {
const { grantType } = this const { grantType } = this
let rids let rids
if (type === 'depart') { if (type === 'depart') {
rids = [ rids = [
...params.department.map((rid) => { ...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid) const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid } return { rid, name: _find?.department_name ?? rid }
}), }),
...params.user.map((rid) => { ...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid) const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid } return { rid, name: _find?.nickname ?? rid }
}), }),
] ]
} }
if (type === 'role') { if (type === 'role') {
rids = [ rids = [
...params.map((role) => { ...params.map((role) => {
return { rid: role.id, name: role.name } return { rid: role.id, name: role.name }
}), }),
] ]
} }
if (grantType === 'ci_type') { if (grantType === 'ci_type') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
conifg: false, conifg: false,
grant: false, grant: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'ci') { if (grantType === 'ci') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
read_attr: false, read_attr: false,
read_ci: false, read_ci: false,
create: false, create: false,
update: false, update: false,
delete: false, delete: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'type_relation') { if (grantType === 'type_relation') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
create: false, create: false,
grant: false, grant: false,
delete: false, delete: false,
} }
}) })
) )
} }
if (grantType === 'relation_view') { if (grantType === 'relation_view') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
read: false, read: false,
grant: false, grant: false,
} }
}) })
) )
} }
this.addedRids = rids this.addedRids = rids
this.$nextTick(() => { this.$nextTick(() => {
setTimeout(() => { setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0) this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300) }, 300)
}) })
}, },
openReadGrantModal(col, row) { openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row) this.$refs.readGrantModal.open(col, row)
}, },
updateTableDataRead(row, hasRead) { updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid) const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead }) this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions() this.getFilterPermissions()
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-grant { .cmdb-grant {
position: relative; position: relative;
padding: 24px 24px 0 24px; padding: 24px 24px 0 24px;
overflow: auto; overflow: auto;
.cmdb-grant-title { .cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1]; border-left: 4px solid #custom_colors[color_1];
padding-left: 10px; padding-left: 10px;
} }
} }
</style> </style>
<style lang="less"> <style lang="less">
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-grant { .cmdb-grant {
.grant-button { .grant-button {
padding: 6px 8px; padding: 6px 8px;
color: #custom_colors[color_1]; color: #custom_colors[color_1];
background-color: #custom_colors[color_2]; background-color: #custom_colors[color_2];
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
margin: 15px 0; margin: 15px 0;
display: inline-block; display: inline-block;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
box-shadow: 2px 3px 4px #custom_colors[color_2]; box-shadow: 2px 3px 4px #custom_colors[color_2];
} }
} }
} }
</style> </style>

View File

@ -1,57 +1,67 @@
<template> <template>
<a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose> <a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose>
<EmployeeTransfer <EmployeeTransfer
:isDisabledAllCompany="true" :isDisabledAllCompany="true"
v-if="type === 'depart'" v-if="type === 'depart'"
uniqueKey="acl_rid" uniqueKey="acl_rid"
ref="employeeTransfer" ref="employeeTransfer"
:height="350" :height="350"
/> />
<RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" /> <RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" />
</a-modal> </a-modal>
</template> </template>
<script> <script>
import EmployeeTransfer from '@/components/EmployeeTransfer' import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer' import RoleTransfer from '@/components/RoleTransfer'
export default {
name: 'GrantModal', export default {
components: { EmployeeTransfer, RoleTransfer }, name: 'GrantModal',
data() { components: { EmployeeTransfer, RoleTransfer },
return { props: {
visible: false, customTitle: {
type: 'depart', type: String,
} default: '',
}, },
computed: { },
title() { data() {
if (this.type === 'depart') { return {
return this.$t('cmdb.components.grantUser') visible: false,
} type: 'depart',
return this.$t('cmdb.components.grantRole') }
}, },
}, computed: {
methods: { title() {
open(type) { if (this.customTitle) {
this.visible = true return this.customTitle
this.type = type }
}, if (this.type === 'depart') {
handleOk() { return this.$t('cmdb.components.grantUser')
let params }
if (this.type === 'depart') { return this.$t('cmdb.components.grantRole')
params = this.$refs.employeeTransfer.getValues() },
} },
if (this.type === 'role') { methods: {
params = this.$refs.roleTransfer.getValues() open(type) {
} this.visible = true
this.handleCancel() this.type = type
this.$emit('handleOk', params, this.type) },
}, handleOk() {
handleCancel() { let params
this.visible = false if (this.type === 'depart') {
}, params = this.$refs.employeeTransfer.getValues()
}, }
} if (this.type === 'role') {
</script> params = this.$refs.roleTransfer.getValues()
}
<style></style> this.handleCancel()
this.$emit('handleOk', params, this.type)
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@ -1,57 +1,57 @@
<template> <template>
<a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }"> <a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }">
<GrantComp <GrantComp
:resourceType="resourceType" :resourceType="resourceType"
:app_id="app_id" :app_id="app_id"
:cmdbGrantType="cmdbGrantType" :cmdbGrantType="cmdbGrantType"
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:typeRelationIds="typeRelationIds" :typeRelationIds="typeRelationIds"
:CITypeId="CITypeId" :CITypeId="CITypeId"
:isModal="true" :isModal="true"
/> />
</a-modal> </a-modal>
</template> </template>
<script> <script>
import GrantComp from './grantComp.vue' import GrantComp from './grantComp.vue'
export default { export default {
name: 'CMDBGrant', name: 'CMDBGrant',
components: { GrantComp }, components: { GrantComp },
props: { props: {
resourceType: { resourceType: {
type: String, type: String,
default: 'CIType', default: 'CIType',
}, },
app_id: { app_id: {
type: String, type: String,
default: '', default: '',
}, },
}, },
data() { data() {
return { return {
visible: false, visible: false,
resourceTypeName: '', resourceTypeName: '',
typeRelationIds: [], typeRelationIds: [],
cmdbGrantType: '', cmdbGrantType: '',
CITypeId: null, CITypeId: null,
} }
}, },
methods: { methods: {
open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) { open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) {
this.visible = true this.visible = true
this.resourceTypeName = name this.resourceTypeName = name
this.typeRelationIds = typeRelationIds this.typeRelationIds = typeRelationIds
this.cmdbGrantType = cmdbGrantType this.cmdbGrantType = cmdbGrantType
this.CITypeId = CITypeId this.CITypeId = CITypeId
}, },
handleOk() { handleOk() {
this.handleCancel() this.handleCancel()
}, },
handleCancel() { handleCancel() {
this.visible = false this.visible = false
}, },
}, },
} }
</script> </script>
<style></style> <style></style>

View File

@ -1,89 +1,89 @@
<template> <template>
<div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal"> <div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal">
<a-tooltip <a-tooltip
v-if="value && isHalfChecked" v-if="value && isHalfChecked"
:title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''" :title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''"
> >
<div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div> <div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div>
</a-tooltip> </a-tooltip>
<a-checkbox v-else :checked="value" /> <a-checkbox v-else :checked="value" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'ReadCheckbox', name: 'ReadCheckbox',
inject: { inject: {
provide_filerPerimissions: { provide_filerPerimissions: {
from: 'filerPerimissions', from: 'filerPerimissions',
}, },
}, },
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
valueKey: { valueKey: {
type: String, type: String,
default: 'read_attr', default: 'read_attr',
}, },
rid: { rid: {
type: Number, type: Number,
default: 0, default: 0,
}, },
}, },
computed: { computed: {
filerPerimissions() { filerPerimissions() {
return this.provide_filerPerimissions() return this.provide_filerPerimissions()
}, },
filterKey() { filterKey() {
if (this.valueKey === 'read_attr') { if (this.valueKey === 'read_attr') {
return 'attr_filter' return 'attr_filter'
} }
return 'ci_filter' return 'ci_filter'
}, },
isHalfChecked() { isHalfChecked() {
if (this.filerPerimissions[this.rid]) { if (this.filerPerimissions[this.rid]) {
const _tempValue = this.filerPerimissions[this.rid][this.filterKey] const _tempValue = this.filerPerimissions[this.rid][this.filterKey]
return !!(_tempValue && _tempValue.length) return !!(_tempValue && _tempValue.length)
} }
return false return false
}, },
}, },
methods: { methods: {
openReadGrantModal() { openReadGrantModal() {
this.$emit('openReadGrantModal') this.$emit('openReadGrantModal')
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less'; @import '~@/style/static.less';
.read-checkbox { .read-checkbox {
.read-checkbox-half-checked { .read-checkbox-half-checked {
width: 16px; width: 16px;
height: 16px; height: 16px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 0; width: 0;
height: 0; height: 0;
// background-color: #custom_colors[color_1]; // background-color: #custom_colors[color_1];
border-radius: 2px; border-radius: 2px;
border: 14px solid transparent; border: 14px solid transparent;
border-left-color: #custom_colors[color_1]; border-left-color: #custom_colors[color_1];
transform: rotate(225deg); transform: rotate(225deg);
top: -16px; top: -16px;
left: -17px; left: -17px;
} }
} }
} }
</style> </style>

View File

@ -1,205 +1,212 @@
<template> <template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel"> <a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio <CustomRadio
:radioList="[ :radioList="[
{ value: 1, label: $t('cmdb.components.all') }, { value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' }, { value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') }, { value: 3, label: $t('cmdb.components.none') },
]" ]"
v-model="radioValue" :value="radioValue"
> @change="changeRadioValue"
<template slot="extra_2" v-if="radioValue === 2"> >
<treeselect <template slot="extra_2" v-if="radioValue === 2">
v-if="colType === 'read_attr'" <treeselect
v-model="selectedAttr" v-if="colType === 'read_attr'"
:multiple="true" v-model="selectedAttr"
:clearable="true" :multiple="true"
searchable :clearable="true"
:options="attrGroup" searchable
:placeholder="$t('cmdb.ciType.selectAttributes')" :options="attrGroup"
value-consists-of="LEAF_PRIORITY" :placeholder="$t('cmdb.ciType.selectAttributes')"
:limit="10" value-consists-of="LEAF_PRIORITY"
:limitText="(count) => `+ ${count}`" :limit="10"
:normalizer=" :limitText="(count) => `+ ${count}`"
(node) => { :normalizer="
return { (node) => {
id: node.name || -1, return {
label: node.alias || node.name || $t('other'), id: node.name || -1,
title: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
children: node.attributes, title: node.alias || node.name || $t('other'),
} children: node.attributes,
} }
" }
appendToBody "
zIndex="1050" appendToBody
> zIndex="1050"
</treeselect> >
<a-form-model </treeselect>
:model="form" <a-form-model
:rules="rules" :model="form"
v-if="colType === 'read_ci'" :rules="rules"
:labelCol="{ span: 2 }" v-if="colType === 'read_ci'"
:wrapperCol="{ span: 10 }" :labelCol="{ span: 2 }"
ref="form" :wrapperCol="{ span: 10 }"
> ref="form"
<a-form-model-item :label="$t('name')" prop="name"> >
<a-input v-model="form.name" /> <a-form-model-item :label="$t('name')" prop="name">
</a-form-model-item> <a-input v-model="form.name" />
<FilterComp </a-form-model-item>
ref="filterComp" <FilterComp
:isDropdown="false" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :isDropdown="false"
@setExpFromFilter="setExpFromFilter" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
:expression="expression" @setExpFromFilter="setExpFromFilter"
/> :expression="expression"
</a-form-model> />
</template> </a-form-model>
</CustomRadio> </template>
</a-modal> </CustomRadio>
</template> </a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType' <script>
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr' import { grantCiType, revokeCiType } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp' import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal', export default {
components: { FilterComp }, name: 'ReadGrantModal',
props: { components: { FilterComp },
CITypeId: { props: {
type: Number, CITypeId: {
default: null, type: Number,
}, default: null,
}, },
inject: { },
provide_attrGroup: { inject: {
from: 'attrGroup', provide_attrGroup: {
}, from: 'attrGroup',
provide_filerPerimissions: { },
from: 'filerPerimissions', provide_filerPerimissions: {
}, from: 'filerPerimissions',
}, },
data() { },
return { data() {
visible: false, return {
colType: '', visible: false,
row: {}, colType: '',
radioValue: 1, row: {},
radioStyle: { radioValue: 1,
display: 'block', radioStyle: {
height: '30px', display: 'block',
lineHeight: '30px', height: '30px',
}, lineHeight: '30px',
selectedAttr: [], },
ruleList: [], selectedAttr: [],
canSearchPreferenceAttrList: [], ruleList: [],
expression: '', canSearchPreferenceAttrList: [],
form: { expression: '',
name: '', form: {
}, name: '',
rules: { },
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }], rules: {
}, name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
} },
}, }
computed: { },
title() { computed: {
if (this.colType === 'read_attr') { title() {
return this.$t('cmdb.components.attributeGrant') if (this.colType === 'read_attr') {
} return this.$t('cmdb.components.attributeGrant')
return this.$t('cmdb.components.ciGrant') }
}, return this.$t('cmdb.components.ciGrant')
attrGroup() { },
return this.provide_attrGroup() attrGroup() {
}, return this.provide_attrGroup()
filerPerimissions() { },
return this.provide_filerPerimissions() filerPerimissions() {
}, return this.provide_filerPerimissions()
filterKey() { },
if (this.colType === 'read_attr') { filterKey() {
return 'attr_filter' if (this.colType === 'read_attr') {
} return 'attr_filter'
return 'ci_filter' }
}, return 'ci_filter'
}, },
methods: { },
async open(colType, row) { methods: {
this.visible = true async open(colType, row) {
this.colType = colType this.visible = true
this.row = row this.colType = colType
if (this.colType === 'read_ci') { this.row = row
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => { this.form = {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6') name: '',
}) }
} if (this.colType === 'read_ci') {
if (this.filerPerimissions[row.rid]) { await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey] this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
if (_tempValue && _tempValue.length) { })
this.radioValue = 2 }
if (this.colType === 'read_attr') { if (this.filerPerimissions[row.rid]) {
this.selectedAttr = _tempValue const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
} else { if (_tempValue && _tempValue.length) {
this.expression = `q=${_tempValue}` this.radioValue = 2
this.form = { if (this.colType === 'read_attr') {
name: this.filerPerimissions[row.rid].name || '', this.selectedAttr = _tempValue
} } else {
this.$nextTick(() => { this.expression = `q=${_tempValue}`
this.$refs.filterComp.visibleChange(true) this.form = {
}) name: this.filerPerimissions[row.rid].name || '',
} }
} this.$nextTick(() => {
} else { this.$refs.filterComp.visibleChange(true)
this.form = { })
name: '', }
} }
} }
}, },
async handleOk() { async handleOk() {
if (this.radioValue === 1) { if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, { await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined, attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined, ci_filter: this.colType === 'read_ci' ? '' : undefined,
}) })
} else if (this.radioValue === 2) { } else if (this.radioValue === 2) {
if (this.colType === 'read_ci') { if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit() this.$refs.filterComp.handleSubmit()
} }
await grantCiType(this.CITypeId, this.row.rid, { await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined, attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined, ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined, name: this.colType === 'read_ci' ? this.form.name : undefined,
}) })
} else { } else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey] const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, { await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined, attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined, ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
}) })
} }
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2) this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel() this.handleCancel()
}, },
handleCancel() { handleCancel() {
this.radioValue = 1 this.radioValue = 1
this.selectedAttr = [] this.selectedAttr = []
if (this.$refs.form) { if (this.$refs.form) {
this.$refs.form.resetFields() this.$refs.form.resetFields()
} }
this.visible = false this.visible = false
}, },
setExpFromFilter(filterExp) { setExpFromFilter(filterExp) {
let expression = '' let expression = ''
if (filterExp) { if (filterExp) {
expression = `q=${filterExp}` expression = `q=${filterExp}`
} }
this.expression = expression this.expression = expression
}, },
}, changeRadioValue(value) {
} if (this.id_filter) {
</script> this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
<style></style> this.radioValue = value
}
},
},
}
</script>
<style></style>

View File

@ -1,98 +1,98 @@
<template> <template>
<div class="ci-relation-grant"> <div class="ci-relation-grant">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
size="mini" size="mini"
stripe stripe
class="ops-stripe-table" class="ops-stripe-table"
:data="tableData" :data="tableData"
:max-height="`${tableHeight}px`" :max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)" :row-style="(params) => getCurrentRowStyle(params, addedRids)"
> >
<vxe-column field="name"></vxe-column> <vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]"> <vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}"> <template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox> <a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<a-space> <a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span> <span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span> <span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space> </a-space>
</div> </div>
</template> </template>
<script> <script>
import { permMap } from './constants.js' import { permMap } from './constants.js'
import { grantRelationView, revokeRelationView } from '../../api/preference.js' import { grantRelationView, revokeRelationView } from '../../api/preference.js'
import { getCurrentRowStyle } from './utils' import { getCurrentRowStyle } from './utils'
export default { export default {
name: 'RelationViewGrant', name: 'RelationViewGrant',
inject: ['loading', 'isModal'], inject: ['loading', 'isModal'],
props: { props: {
resourceTypeName: { resourceTypeName: {
type: String, type: String,
default: '', default: '',
}, },
tableData: { tableData: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
grantType: { grantType: {
type: String, type: String,
default: 'relation_view', default: 'relation_view',
}, },
addedRids: { addedRids: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
columns: ['read', 'grant'], columns: ['read', 'grant'],
} }
}, },
computed: { computed: {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
tableHeight() { tableHeight() {
if (this.isModal) { if (this.isModal) {
return (this.windowHeight - 104) / 2 return (this.windowHeight - 104) / 2
} }
return (this.windowHeight - 104) / 2 - 116 return (this.windowHeight - 104) / 2 - 116
}, },
permMap() { permMap() {
return permMap() return permMap()
} }
}, },
methods: { methods: {
getCurrentRowStyle, getCurrentRowStyle,
grantDepart() { grantDepart() {
this.$emit('grantDepart', this.grantType) this.$emit('grantDepart', this.grantType)
}, },
grantRole() { grantRole() {
this.$emit('grantRole', this.grantType) this.$emit('grantRole', this.grantType)
}, },
handleChange(e, col, row) { handleChange(e, col, row) {
if (e.target.checked) { if (e.target.checked) {
grantRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => { grantRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} else { } else {
revokeRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => { revokeRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} }
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.ci-relation-grant { .ci-relation-grant {
padding: 10px 0; padding: 10px 0;
} }
</style> </style>

View File

@ -0,0 +1,122 @@
<template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')">
<a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t('user')">
<EmployeeTreeSelect
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:multiple="true"
v-model="form.users"
:placeholder="$t('cmdb.serviceTree.userPlaceholder')"
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
/>
</a-form-model-item>
<a-form-model-item :label="$t('role')">
<treeselect
v-model="form.roles"
:multiple="true"
:options="filterAllRoles"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.id,
label: node.name,
}
}
"
appendToBody
zIndex="1050"
:placeholder="$t('cmdb.serviceTree.rolePlaceholder')"
@search-change="searchRole"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import { getAllDepAndEmployee } from '@/api/company'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'RevokeModal',
components: { EmployeeTreeSelect },
data() {
return {
visible: false,
form: {
users: undefined,
roles: undefined,
},
allTreeDepAndEmp: [],
allRoles: [],
filterAllRoles: [],
}
},
provide() {
return {
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
this.filterAllRoles = this.allRoles.slice(0, 100)
},
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
open() {
this.visible = true
this.$nextTick(() => {
this.form = {
users: undefined,
roles: undefined,
}
})
},
handleCancel() {
this.visible = false
},
searchRole(searchQuery) {
this.filterAllRoles = this.allRoles
.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 100)
},
handleOK() {
this.$emit('handleRevoke', this.form)
this.handleCancel()
},
},
}
</script>
<style></style>

View File

@ -1,100 +1,100 @@
<template> <template>
<div class="ci-relation-grant"> <div class="ci-relation-grant">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
size="mini" size="mini"
stripe stripe
class="ops-stripe-table" class="ops-stripe-table"
:data="tableData" :data="tableData"
:max-height="`${tableHeight}px`" :max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)" :row-style="(params) => getCurrentRowStyle(params, addedRids)"
> >
<vxe-column field="name"></vxe-column> <vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]"> <vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}"> <template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox> <a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<a-space> <a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span> <span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span> <span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space> </a-space>
</div> </div>
</template> </template>
<script> <script>
import { permMap } from './constants.js' import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js' import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
import { getCurrentRowStyle } from './utils' import { getCurrentRowStyle } from './utils'
export default { export default {
name: 'TypeRelationGrant', name: 'TypeRelationGrant',
inject: ['loading', 'isModal'], inject: ['loading', 'isModal'],
props: { props: {
tableData: { tableData: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
grantType: { grantType: {
type: String, type: String,
default: 'type_relation', default: 'type_relation',
}, },
typeRelationIds: { typeRelationIds: {
type: Array, type: Array,
default: null, default: null,
}, },
addedRids: { addedRids: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
columns: ['create', 'grant', 'delete'], columns: ['create', 'grant', 'delete'],
} }
}, },
computed: { computed: {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
tableHeight() { tableHeight() {
if (this.isModal) { if (this.isModal) {
return (this.windowHeight - 104) / 2 return (this.windowHeight - 104) / 2
} }
return (this.windowHeight - 104) / 2 - 116 return (this.windowHeight - 104) / 2 - 116
}, },
permMap() { permMap() {
return permMap() return permMap()
} }
}, },
methods: { methods: {
getCurrentRowStyle, getCurrentRowStyle,
grantDepart() { grantDepart() {
this.$emit('grantDepart', this.grantType) this.$emit('grantDepart', this.grantType)
}, },
grantRole() { grantRole() {
this.$emit('grantRole', this.grantType) this.$emit('grantRole', this.grantType)
}, },
handleChange(e, col, row) { handleChange(e, col, row) {
const first = this.typeRelationIds[0] const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1] const second = this.typeRelationIds[1]
if (e.target.checked) { if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => { grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} else { } else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => { revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData') this.$emit('getTableData')
}) })
} }
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.ci-relation-grant { .ci-relation-grant {
padding: 10px 0; padding: 10px 0;
} }
</style> </style>

View File

@ -1,4 +1,4 @@
export const getCurrentRowStyle = ({ row }, addedRids) => { export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid) const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : '' return idx > -1 ? 'background-color:#E0E7FF!important' : ''
} }

View File

@ -1,301 +1,305 @@
<template> <template>
<div> <div>
<div id="search-form-bar" class="search-form-bar"> <div id="search-form-bar" class="search-form-bar">
<div :style="{ display: 'inline-flex', alignItems: 'center' }"> <div :style="{ display: 'inline-flex', alignItems: 'center' }">
<a-space> <a-space>
<treeselect <treeselect
v-if="type === 'resourceSearch'" v-if="type === 'resourceSearch'"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }" :style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }"
v-model="currenCiType" v-model="currenCiType"
:multiple="true" :multiple="true"
:clearable="true" :clearable="true"
searchable searchable
:options="ciTypeGroup" :options="ciTypeGroup"
:limit="1" :limit="1"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')" :placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup" @close="closeCiTypeGroup"
@open="openCiTypeGroup" @open="openCiTypeGroup"
@input="inputCiTypeGroup" @input="inputCiTypeGroup"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.id || -1, id: node.id || -1,
label: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'), title: node.alias || node.name || $t('other'),
children: node.ci_types, children: node.ci_types,
} }
} }
" "
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
{{ node.label }} {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<a-input <a-input
v-model="fuzzySearch" v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '244px' }" :style="{ display: 'inline-block', width: '244px' }"
:placeholder="$t('cmdb.components.pleaseSearch')" :placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh" @pressEnter="emitRefresh"
class="ops-input ops-input-radius" class="ops-input ops-input-radius"
> >
<a-icon <a-icon
type="search" type="search"
slot="suffix" slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }" :style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }"
@click="emitRefresh" @click="emitRefresh"
/> />
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px' }"> <a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title"> <template slot="title">
{{ $t('cmdb.components.ciSearchTips') }} {{ $t('cmdb.components.ciSearchTips') }}
</template> </template>
<a><a-icon type="question-circle"/></a> <a><a-icon type="question-circle"/></a>
</a-tooltip> </a-tooltip>
</a-input> </a-input>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter" @setExpFromFilter="setExpFromFilter"
:expression="expression" :expression="expression"
placement="bottomLeft" placement="bottomLeft"
> >
<div slot="popover_item" class="search-form-bar-filter"> <div slot="popover_item" class="search-form-bar-filter">
<a-icon class="search-form-bar-filter-icon" type="filter" /> <a-icon class="search-form-bar-filter-icon" type="filter" />
{{ $t('cmdb.components.conditionFilter') }} {{ $t('cmdb.components.conditionFilter') }}
<a-icon class="search-form-bar-filter-icon" type="down" /> <a-icon class="search-form-bar-filter-icon" type="down" />
</div> </div>
</FilterComp> </FilterComp>
<a-input <a-input
v-if="isShowExpression" v-if="isShowExpression"
v-model="expression" v-model="expression"
v-show="!selectedRowKeys.length" v-show="!selectedRowKeys.length"
@focus=" @focus="
() => { () => {
isFocusExpression = true isFocusExpression = true
} }
" "
@blur=" @blur="
() => { () => {
isFocusExpression = false isFocusExpression = false
} }
" "
class="ci-searchform-expression" class="ci-searchform-expression"
:style="{ width }" :style="{ width }"
:placeholder="placeholder" :placeholder="placeholder"
@keyup.enter="emitRefresh" @keyup.enter="emitRefresh"
> >
<a-icon slot="suffix" type="copy" @click="handleCopyExpression" /> <a-icon slot="suffix" type="copy" @click="handleCopyExpression" />
</a-input> </a-input>
<slot></slot> <slot></slot>
</a-space> </a-space>
</div> </div>
<a-space> <a-space>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button> <slot name="extraContent"></slot>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'"> <a-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
@click=" <a
() => { @click="
$refs.metadataDrawer.open(typeId) () => {
} $refs.metadataDrawer.open(typeId)
" }
><a-icon "
v-if="type === 'relationView'" ><a-icon
type="question-circle" v-if="type === 'relationView'"
/></a> type="question-circle"
</a-tooltip> /></a>
</a-space> </a-tooltip>
</div> </a-space>
<MetadataDrawer ref="metadataDrawer" /> </div>
</div> <MetadataDrawer ref="metadataDrawer" />
</template> </div>
</template>
<script>
import _ from 'lodash' <script>
import Treeselect from '@riophae/vue-treeselect' import _ from 'lodash'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue' import Treeselect from '@riophae/vue-treeselect'
import FilterComp from '@/components/CMDBFilterComp' import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import { getCITypeGroups } from '../../api/ciTypeGroup' import FilterComp from '@/components/CMDBFilterComp'
export default { import { getCITypeGroups } from '../../api/ciTypeGroup'
name: 'SearchForm', export default {
components: { MetadataDrawer, FilterComp, Treeselect }, name: 'SearchForm',
props: { components: { MetadataDrawer, FilterComp, Treeselect },
preferenceAttrList: { props: {
type: Array, preferenceAttrList: {
required: true, type: Array,
}, required: true,
isShowExpression: { },
type: Boolean, isShowExpression: {
default: true, type: Boolean,
}, default: true,
typeId: { },
type: Number, typeId: {
default: null, type: Number,
}, default: null,
type: { },
type: String, type: {
default: '', type: String,
}, default: '',
selectedRowKeys: { },
type: Array, selectedRowKeys: {
default: () => [], type: Array,
}, default: () => [],
}, },
data() { },
return { data() {
// Advanced Search Expand/Close return {
advanced: false, // Advanced Search Expand/Close
queryParam: {}, advanced: false,
isFocusExpression: false, queryParam: {},
expression: '', isFocusExpression: false,
fuzzySearch: '', expression: '',
currenCiType: [], fuzzySearch: '',
ciTypeGroup: [], currenCiType: [],
lastCiType: [], ciTypeGroup: [],
} lastCiType: [],
}, }
},
computed: {
placeholder() { computed: {
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr') placeholder() {
}, return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
width() { },
return '200px' width() {
}, return '200px'
canSearchPreferenceAttrList() { },
return this.preferenceAttrList.filter((item) => item.value_type !== '6') canSearchPreferenceAttrList() {
}, return this.preferenceAttrList.filter((item) => item.value_type !== '6')
}, },
watch: { },
'$route.path': function(newValue, oldValue) { watch: {
this.queryParam = {} '$route.path': function(newValue, oldValue) {
this.expression = '' this.queryParam = {}
this.fuzzySearch = '' this.expression = ''
}, this.fuzzySearch = ''
}, },
inject: { },
setPreferenceSearchCurrent: { inject: {
from: 'setPreferenceSearchCurrent', setPreferenceSearchCurrent: {
default: null, from: 'setPreferenceSearchCurrent',
}, default: null,
}, },
mounted() { },
if (this.type === 'resourceSearch') { mounted() {
this.getCITypeGroups() if (this.type === 'resourceSearch') {
} this.getCITypeGroups()
}, }
methods: { },
getCITypeGroups() { methods: {
getCITypeGroups({ need_other: true }).then((res) => { // toggleAdvanced() {
this.ciTypeGroup = res // this.advanced = !this.advanced
.filter((item) => item.ci_types && item.ci_types.length) // },
.map((item) => { getCITypeGroups() {
item.id = `parent_${item.id || -1}` getCITypeGroups({ need_other: true }).then((res) => {
return { ..._.cloneDeep(item) } this.ciTypeGroup = res
}) .filter((item) => item.ci_types && item.ci_types.length)
}) .map((item) => {
}, item.id = `parent_${item.id || -1}`
reset() { return { ..._.cloneDeep(item) }
this.queryParam = {} })
this.expression = '' })
this.fuzzySearch = '' },
this.currenCiType = [] reset() {
this.emitRefresh() this.queryParam = {}
}, this.expression = ''
setExpFromFilter(filterExp) { this.fuzzySearch = ''
const regSort = /(?<=sort=).+/g this.currenCiType = []
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined this.emitRefresh()
let expression = '' },
if (filterExp) { setExpFromFilter(filterExp) {
expression = `q=${filterExp}` const regSort = /(?<=sort=).+/g
} const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
if (expSort) { let expression = ''
expression += `&sort=${expSort}` if (filterExp) {
} expression = `q=${filterExp}`
this.expression = expression }
this.emitRefresh() if (expSort) {
}, expression += `&sort=${expSort}`
handleSubmit() { }
this.$refs.filterComp.handleSubmit() this.expression = expression
}, this.emitRefresh()
openCiTypeGroup() { },
this.lastCiType = _.cloneDeep(this.currenCiType) handleSubmit() {
}, this.$refs.filterComp.handleSubmit()
closeCiTypeGroup(value) { },
if (!_.isEqual(value, this.lastCiType)) { openCiTypeGroup() {
this.$emit('updateAllAttributesList', value) this.lastCiType = _.cloneDeep(this.currenCiType)
} },
}, closeCiTypeGroup(value) {
inputCiTypeGroup(value) { if (!_.isEqual(value, this.lastCiType)) {
console.log(value) this.$emit('updateAllAttributesList', value)
if (!value || !value.length) { }
this.$emit('updateAllAttributesList', value) },
} inputCiTypeGroup(value) {
}, console.log(value)
emitRefresh() { if (!value || !value.length) {
if (this.setPreferenceSearchCurrent) { this.$emit('updateAllAttributesList', value)
this.setPreferenceSearchCurrent(null) }
} },
this.$nextTick(() => { emitRefresh() {
this.$emit('refresh', true) if (this.setPreferenceSearchCurrent) {
}) this.setPreferenceSearchCurrent(null)
}, }
handleCopyExpression() { this.$nextTick(() => {
this.$emit('copyExpression') this.$emit('refresh', true)
}, })
}, },
} handleCopyExpression() {
</script> this.$emit('copyExpression')
<style lang="less"> },
@import '../../views/index.less'; },
.ci-searchform-expression { }
> input { </script>
border-bottom: 2px solid #d9d9d9; <style lang="less">
border-top: none; @import '../../views/index.less';
border-left: none; .ci-searchform-expression {
border-right: none; > input {
&:hover, border-bottom: 2px solid #d9d9d9;
&:focus { border-top: none;
border-bottom: 2px solid #2f54eb; border-left: none;
} border-right: none;
&:focus { &:hover,
box-shadow: 0 2px 2px -2px #1f78d133; &:focus {
} border-bottom: 2px solid #2f54eb;
} }
.ant-input-suffix { &:focus {
color: #2f54eb; box-shadow: 0 2px 2px -2px #1f78d133;
cursor: pointer; }
} }
} .ant-input-suffix {
.cmdb-search-form { color: #2f54eb;
.ant-form-item-label { cursor: pointer;
overflow: hidden; }
text-overflow: ellipsis; }
white-space: nowrap; .cmdb-search-form {
} .ant-form-item-label {
} overflow: hidden;
</style> text-overflow: ellipsis;
white-space: nowrap;
<style lang="less" scoped> }
@import '~@/style/static.less'; }
</style>
.search-form-bar {
margin-bottom: 10px; <style lang="less" scoped>
display: flex; @import '~@/style/static.less';
justify-content: space-between;
align-items: center; .search-form-bar {
.search-form-bar-filter { margin-bottom: 10px;
.ops_display_wrapper(); display: flex;
.search-form-bar-filter-icon { justify-content: space-between;
color: #custom_colors[color_1]; align-items: center;
font-size: 12px; .search-form-bar-filter {
} .ops_display_wrapper();
} .search-form-bar-filter-icon {
} color: #custom_colors[color_1];
</style> font-size: 12px;
}
}
}
</style>

View File

@ -1,492 +1,503 @@
const cmdb_en = { const cmdb_en = {
relation: 'Relation', relation: 'Relation',
attribute: 'Attributes', attribute: 'Attributes',
menu: { menu: {
views: 'Views', views: 'Views',
config: 'Configuration', config: 'Configuration',
backend: 'Management', backend: 'Management',
ciTable: 'Resource Views', ciTable: 'Resource Views',
ciTree: 'Tree Views', ciTree: 'Tree Views',
ciSearch: 'Search', ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool', adCIs: 'AutoDiscovery Pool',
preference: 'Preference', preference: 'Preference',
batchUpload: 'Batch Import', batchUpload: 'Batch Import',
citypeManage: 'Modeling', citypeManage: 'Modeling',
backendManage: 'Backend', backendManage: 'Backend',
customDashboard: 'Custom Dashboard', customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree', serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation', citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit', operationHistory: 'Operation Audit',
relationType: 'Relation Type', relationType: 'Relation Type',
ad: 'AutoDiscovery', ad: 'AutoDiscovery',
cidetail: 'CI Detail' cidetail: 'CI Detail'
}, },
ciType: { ciType: {
ciType: 'CIType', ciType: 'CIType',
attributes: 'Attributes', attributes: 'Attributes',
relation: 'Relation', relation: 'Relation',
trigger: 'Triggers', trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery', attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery', relationAD: 'Relation AutoDiscovery',
grant: 'Grant', grant: 'Grant',
addGroup: 'New Group', addGroup: 'New Group',
editGroup: 'Edit Group', editGroup: 'Edit Group',
group: 'Group', group: 'Group',
attributeLibray: 'Attribute Library', attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group', addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType', addCIType: 'Add CIType',
editGroupName: 'Edit group name', editGroupName: 'Edit group name',
deleteGroup: 'Delete this group', deleteGroup: 'Delete this group',
CITypeName: 'Name(English)', CITypeName: 'Name(English)',
English: 'English', English: 'English',
inputAttributeName: 'Please enter the attribute name', inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)', attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType', editCIType: 'Edit CIType',
defaultSort: 'Default sort', defaultSort: 'Default sort',
selectDefaultOrderAttr: 'Select default sorting attributes', selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order', asec: 'Forward order',
desc: 'Reverse order', desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies', uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier', uniqueKeySelect: 'Please select a unique identifier',
notfound: 'Can\'t find what you want?', uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', notfound: 'Can\'t find what you want?',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
uploading: 'Uploading', confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploadFailed: 'Upload failed, please try again later', uploading: 'Uploading',
addPlugin: 'New plugin', uploadFailed: 'Upload failed, please try again later',
deletePlugin: 'Delete plugin', addPlugin: 'New plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]', deletePlugin: 'Delete plugin',
attributeMap: 'Attribute mapping', confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
autoDiscovery: 'AutoDiscovery', attributeMap: 'Attribute mapping',
node: 'Node', autoDiscovery: 'AutoDiscovery',
adExecConfig: 'Execute configuration', node: 'Node',
adExecTarget: 'Execute targets', adExecConfig: 'Execute configuration',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x', adExecTarget: 'Execute targets',
selectFromCMDBTips: 'Select from CMDB ', oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
adAutoInLib: 'Save as CI auto', selectFromCMDBTips: 'Select from CMDB ',
adInterval: 'Collection frequency', adAutoInLib: 'Save as CI auto',
byInterval: 'by interval', adInterval: 'Collection frequency',
allNodes: 'All nodes', byInterval: 'by interval',
specifyNodes: 'Specify Node', allNodes: 'All nodes',
specifyNodesTips: 'Please fill in the specify node!', specifyNodes: 'Specify Node',
username: 'Username', specifyNodesTips: 'Please fill in the specify node!',
password: 'Password', username: 'Username',
link: 'Link', password: 'Password',
list: 'List', link: 'Link',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.', list: 'List',
computeForAllCITips: 'All CI trigger computes', listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?', computeForAllCITips: 'All CI trigger computes',
isUnique: 'Is it unique', confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
unique: 'Unique', isUnique: 'Is it unique',
isChoice: 'Choiced', unique: 'Unique',
defaultShow: 'Default Display', isChoice: 'Choiced',
defaultShowTips: 'The CI instance table displays this field by default', defaultShow: 'Default Display',
isSortable: 'Sortable', defaultShowTips: 'The CI instance table displays this field by default',
isIndex: 'Indexed', isSortable: 'Sortable',
index: 'Index', isIndex: 'Indexed',
indexTips: 'Fields can be used for retrieval to speed up queries', index: 'Index',
confirmDelete: 'Confirm to delete [{name}]?', indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete2: 'Confirm to delete?', confirmDelete: 'Confirm to delete [{name}]?',
computeSuccess: 'Triggered successfully!', confirmDelete2: 'Confirm to delete?',
basicConfig: 'Basic Settings', computeSuccess: 'Triggered successfully!',
AttributeName: 'Name(English)', basicConfig: 'Basic Settings',
DataType: 'Data Type', AttributeName: 'Name(English)',
defaultValue: 'Default value', DataType: 'Data Type',
autoIncID: 'Auto-increment ID', defaultValue: 'Default value',
customTime: 'Custom time', autoIncID: 'Auto-increment ID',
advancedSettings: 'Advanced Settings', customTime: 'Custom time',
font: 'Font', advancedSettings: 'Advanced Settings',
color: 'Color', font: 'Font',
choiceValue: 'Predefined value', color: 'Color',
computedAttribute: 'Computed Attribute', choiceValue: 'Predefined value',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}', computedAttribute: 'Computed Attribute',
addAttribute: 'New attribute', computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
existedAttributes: 'Already have attributes', addAttribute: 'New attribute',
editAttribute: 'Edit attribute', existedAttributes: 'Already have attributes',
addAttributeTips1: 'If sorting is selected, it must also be selected!', editAttribute: 'Edit attribute',
uniqueConstraint: 'Unique Constraint', addAttributeTips1: 'If sorting is selected, it must also be selected!',
up: 'Move up', uniqueConstraint: 'Unique Constraint',
down: 'Move down', up: 'Move up',
selectAttribute: 'Select Attribute', down: 'Move down',
groupExisted: 'Group name already exists', selectAttribute: 'Select Attribute',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!', groupExisted: 'Group name already exists',
buildinAttribute: 'built-in attributes', attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
expr: 'Expression', buildinAttribute: 'built-in attributes',
code: 'Code', expr: 'Expression',
apply: 'apply', code: 'Code',
continueAdd: 'Keep adding', apply: 'apply',
filter: 'Filter', continueAdd: 'Keep adding',
choiceOther: 'Other CIType Attributes', filter: 'Filter',
choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]', choiceOther: 'Other CIType Attributes',
selectCIType: 'Please select a CMDB CIType', choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]',
selectCITypeAttributes: 'Please select CIType attributes', selectCIType: 'Please select a CMDB CIType',
selectAttributes: 'Please select attributes', selectCITypeAttributes: 'Please select CIType attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []', selectAttributes: 'Please select attributes',
valueExisted: 'The current value already exists!', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
addRelation: 'Add Relation', valueExisted: 'The current value already exists!',
sourceCIType: 'Source CIType', addRelation: 'Add Relation',
sourceCITypeTips: 'Please select Source CIType', sourceCIType: 'Source CIType',
dstCIType: 'Target CIType', sourceCITypeTips: 'Please select Source CIType',
dstCITypeTips: 'Please select target CIType', dstCIType: 'Target CIType',
relationType: 'Relation Type', dstCITypeTips: 'Please select target CIType',
relationTypeTips: 'Please select relation type', relationType: 'Relation Type',
isParent: 'is parent', relationTypeTips: 'Please select relation type',
relationConstraint: 'Constraints', isParent: 'is parent',
relationConstraintTips: 'please select a relationship constraint', relationConstraint: 'Constraints',
one2Many: 'One to Many', relationConstraintTips: 'please select a relationship constraint',
one2One: 'One to One', one2Many: 'One to Many',
many2Many: 'Many to Many', one2One: 'One to One',
basicInfo: 'Basic Information', many2Many: 'Many to Many',
nameInputTips: 'Please enter name', basicInfo: 'Basic Information',
triggerDataChange: 'Data changes', nameInputTips: 'Please enter name',
triggerDate: 'Date attribute', triggerDataChange: 'Data changes',
triggerEnable: 'Turn on', triggerDate: 'Date attribute',
descInput: 'Please enter remarks', triggerEnable: 'Turn on',
triggerCondition: 'Triggering conditions', descInput: 'Please enter remarks',
addInstance: 'Add new instance', triggerCondition: 'Triggering conditions',
deleteInstance: 'Delete instance', addInstance: 'Add new instance',
changeInstance: 'Instance changes', deleteInstance: 'Delete instance',
selectMutipleAttributes: 'Please select attributes (multiple selections)', changeInstance: 'Instance changes',
selectSingleAttribute: 'Please select an attribute (single choice)', selectMutipleAttributes: 'Please select attributes (multiple selections)',
beforeDays: 'ahead of time', selectSingleAttribute: 'Please select an attribute (single choice)',
days: 'Days', beforeDays: 'ahead of time',
notifyAt: 'Send time', days: 'Days',
notify: 'Notify', notifyAt: 'Send time',
triggerAction: 'Trigger action', notify: 'Notify',
receivers: 'Recipients', triggerAction: 'Trigger action',
emailTips: 'Please enter your email address, separate multiple email addresses with ;', receivers: 'Recipients',
customEmail: 'Custom recipients', emailTips: 'Please enter your email address, separate multiple email addresses with ;',
notifySubject: 'Notification title', customEmail: 'Custom recipients',
notifySubjectTips: 'Please enter notification title', notifySubject: 'Notification title',
notifyContent: 'Content', notifySubjectTips: 'Please enter notification title',
notifyMethod: 'Notify methods', notifyContent: 'Content',
botSelect: 'Please select a robot', notifyMethod: 'Notify methods',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}', botSelect: 'Please select a robot',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}', refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger', webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
editTriggerTitle: 'Edit trigger {name}', newTrigger: 'Add trigger',
newTriggerTitle: 'Add trigger {name}', editTriggerTitle: 'Edit trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?', newTriggerTitle: 'Add trigger {name}',
int: 'Integer', confirmDeleteTrigger: 'Are you sure to delete this trigger?',
float: 'Float', int: 'Integer',
text: 'Text', float: 'Float',
datetime: 'DateTime', text: 'Text',
date: 'Date', datetime: 'DateTime',
time: 'Time', date: 'Date',
json: 'JSON', time: 'Time',
event: 'Event', json: 'JSON',
reg: 'Regex', event: 'Event',
isInherit: 'Inherit', reg: 'Regex',
inheritType: 'Inherit Type', isInherit: 'Inherit',
inheritTypePlaceholder: 'Please select inherit types', inheritType: 'Inherit Type',
inheritFrom: 'inherit from {name}', inheritTypePlaceholder: 'Please select inherit types',
groupInheritFrom: 'Please go to the {name} for modification' inheritFrom: 'inherit from {name}',
}, groupInheritFrom: 'Please go to the {name} for modification'
components: { },
unselectAttributes: 'Unselected', components: {
selectAttributes: 'Selected', unselectAttributes: 'Unselected',
downloadCI: 'Export data', selectAttributes: 'Selected',
filename: 'Filename', downloadCI: 'Export data',
filenameInputTips: 'Please enter filename', filename: 'Filename',
saveType: 'Save type', filenameInputTips: 'Please enter filename',
saveTypeTips: 'Please select save type', saveType: 'Save type',
xlsx: 'Excel workbook (*.xlsx)', saveTypeTips: 'Please select save type',
csv: 'CSV (comma separated) (*.csv)', xlsx: 'Excel workbook (*.xlsx)',
html: 'Web page (*.html)', csv: 'CSV (comma separated) (*.csv)',
xml: 'XML data (*.xml)', html: 'Web page (*.html)',
txt: 'Text file (tab delimited) (*.txt)', xml: 'XML data (*.xml)',
grantUser: 'Grant User/Department', txt: 'Text file (tab delimited) (*.txt)',
grantRole: 'Grant Role', grantUser: 'Grant User/Department',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?', grantRole: 'Grant Role',
readAttribute: 'View Attributes', confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readCI: 'View CIs', readAttribute: 'View Attributes',
config: 'Configuration', readCI: 'View CIs',
ciTypeGrant: 'Grant CIType', config: 'Configuration',
ciGrant: 'Grant CI', ciTypeGrant: 'Grant CIType',
attributeGrant: 'Grant Attribute', ciGrant: 'Grant CI',
relationGrant: 'Grant Relation', attributeGrant: 'Grant Attribute',
perm: 'Permissions', relationGrant: 'Grant Relation',
all: 'All', perm: 'Permissions',
customize: 'Customize', all: 'All',
none: 'None', customize: 'Customize',
customizeFilterName: 'Please enter a custom filter name', none: 'None',
colorPickerError: 'Initialization color format error, use #fff or rgb format', customizeFilterName: 'Please enter a custom filter name',
example: 'Example value', colorPickerError: 'Initialization color format error, use #fff or rgb format',
aliyun: 'aliyun', example: 'Example value',
tencentcloud: 'Tencent Cloud', aliyun: 'aliyun',
huaweicloud: 'Huawei Cloud', tencentcloud: 'Tencent Cloud',
beforeChange: 'Before change', huaweicloud: 'Huawei Cloud',
afterChange: 'After change', beforeChange: 'Before change',
noticeContentTips: 'Please enter notification content', afterChange: 'After change',
saveQuery: 'Save Filters', noticeContentTips: 'Please enter notification content',
pleaseSearch: 'Please search', saveQuery: 'Save Filters',
conditionFilter: 'Conditional filtering', pleaseSearch: 'Please search',
attributeDesc: 'Attribute Description', conditionFilter: 'Conditional filtering',
ciSearchTips: '1. JSON attributes cannot be searched<br />2. If the search content includes commas, they need to be escaped,<br />3. Only index attributes are searched, non-index attributes use conditional filtering', attributeDesc: 'Attribute Description',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*', ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
subCIType: 'Subscription CIType', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
already: 'already', subCIType: 'Subscription CIType',
not: 'not', already: 'already',
sub: 'subscription', not: 'not',
selectBelow: 'Please select below', sub: 'subscription',
subSuccess: 'Subscription successful', selectBelow: 'Please select below',
selectMethods: 'Please select a method', subSuccess: 'Subscription successful',
noAuthRequest: 'No certification requested yet', selectMethods: 'Please select a method',
noParamRequest: 'No parameter certification yet', noAuthRequest: 'No certification requested yet',
requestParam: 'Request parameters', noParamRequest: 'No parameter certification yet',
param: 'Parameter{param}', requestParam: 'Request parameters',
value: 'Value{value}', param: 'Parameter{param}',
clear: 'Clear', value: 'Value{value}',
}, clear: 'Clear',
batch: { },
downloadFailed: 'Download failed', batch: {
unselectCIType: 'No CIType selected yet', downloadFailed: 'Download failed',
pleaseUploadFile: 'Please upload files', unselectCIType: 'No CIType selected yet',
batchUploadCanceled: 'Batch upload canceled', pleaseUploadFile: 'Please upload files',
selectCITypeTips: 'Please select CIType', batchUploadCanceled: 'Batch upload canceled',
downloadTemplate: 'Download Template', selectCITypeTips: 'Please select CIType',
drawTips: 'Click or drag files here to upload!', downloadTemplate: 'Download Template',
supportFileTypes: 'Supported file types: xls, xlsx', drawTips: 'Click or drag files here to upload!',
uploadResult: 'Upload results', supportFileTypes: 'Supported file types: xls, xlsx',
total: 'total', uploadResult: 'Upload results',
successItems: 'items, succeeded', total: 'total',
failedItems: 'items, failed', successItems: 'items, succeeded',
items: 'items', failedItems: 'items, failed',
errorTips: 'Error message', items: 'items',
requestFailedTips: 'An error occurred with the request, please try again later', errorTips: 'Error message',
requestSuccessTips: 'Upload completed', requestFailedTips: 'An error occurred with the request, please try again later',
}, requestSuccessTips: 'Upload completed',
preference: { },
mySub: 'My Subscription', preference: {
sub: 'Subscribe', mySub: 'My Subscription',
cancelSub: 'Unsubscribe', sub: 'Subscribe',
editSub: 'Edit subscription', cancelSub: 'Unsubscribe',
peopleSub: ' people subscribed', editSub: 'Edit subscription',
noSub: 'No subscribed', peopleSub: ' people subscribed',
cancelSubSuccess: 'Unsubscribe successfully', noSub: 'No subscribed',
confirmcancelSub: 'Are you sure to cancel your subscription?', cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?', confirmcancelSub: 'Are you sure to cancel your subscription?',
of: 'of', confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
hoursAgo: 'hours ago', of: 'of',
daysAgo: 'days ago', hoursAgo: 'hours ago',
monthsAgo: 'month ago', daysAgo: 'days ago',
yearsAgo: 'years ago', monthsAgo: 'month ago',
just: 'just now', yearsAgo: 'years ago',
}, just: 'just now',
custom_dashboard: { },
charts: 'Chart', custom_dashboard: {
newChart: 'Add Chart', charts: 'Chart',
editChart: 'Edit Chart', newChart: 'Add Chart',
title: 'Title', editChart: 'Edit Chart',
titleTips: 'Please enter a chart title', title: 'Title',
calcIndicators: 'Counter', titleTips: 'Please enter a chart title',
dimensions: 'Dimensions', calcIndicators: 'Counter',
selectDimensions: 'Please select a dimension', dimensions: 'Dimensions',
quantity: 'Quantity', selectDimensions: 'Please select a dimension',
childCIType: 'Relational CIType', quantity: 'Quantity',
level: 'Level', childCIType: 'Relational CIType',
levelTips: 'Please enter the relationship level', level: 'Level',
preview: 'Preview', levelTips: 'Please enter the relationship level',
showIcon: 'Display icon', preview: 'Preview',
chartType: 'Chart Type', showIcon: 'Display icon',
dataFilter: 'Data Filtering', chartType: 'Chart Type',
format: 'Formats', dataFilter: 'Data Filtering',
fontColor: 'Font Color', format: 'Formats',
backgroundColor: 'Background', fontColor: 'Font Color',
chartColor: 'Chart Color', backgroundColor: 'Background',
chartLength: 'Length', chartColor: 'Chart Color',
barType: 'Bar Type', chartLength: 'Length',
stackedBar: 'Stacked Bar', barType: 'Bar Type',
multipleSeriesBar: 'Multiple Series Bar ', stackedBar: 'Stacked Bar',
axis: 'Axis', multipleSeriesBar: 'Multiple Series Bar ',
direction: 'Direction', axis: 'Axis',
lowerShadow: 'Lower Shadow', direction: 'Direction',
count: 'Indicator', lowerShadow: 'Lower Shadow',
bar: 'Bar', count: 'Indicator',
line: 'Line', bar: 'Bar',
pie: 'Pie', line: 'Line',
table: 'Table', pie: 'Pie',
default: 'default', table: 'Table',
relation: 'Relation', default: 'default',
noCustomDashboard: 'The administrator has not customized the dashboard yet', relation: 'Relation',
}, noCustomDashboard: 'The administrator has not customized the dashboard yet',
preference_relation: { },
newServiceTree: 'Add ServiceTree', preference_relation: {
serviceTreeName: 'Name', newServiceTree: 'Add ServiceTree',
public: 'Public', serviceTreeName: 'Name',
saveLayout: 'Save Layout', public: 'Public',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!', saveLayout: 'Save Layout',
tips1: 'Cannot form a view with the currently selected node, please select again!', childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips2: 'Please enter the new serviceTree name!', tips1: 'Cannot form a view with the currently selected node, please select again!',
tips3: 'Please select at least two nodes!', tips2: 'Please enter the new serviceTree name!',
}, tips3: 'Please select at least two nodes!',
history: { },
ciChange: 'CI', history: {
relationChange: 'Relation', ciChange: 'CI',
ciTypeChange: 'CIType', relationChange: 'Relation',
triggerHistory: 'Triggers', ciTypeChange: 'CIType',
opreateTime: 'Operate Time', triggerHistory: 'Triggers',
user: 'User', opreateTime: 'Operate Time',
userTips: 'Enter filter username', user: 'User',
filter: 'Search', userTips: 'Enter filter username',
filterOperate: 'fitler operation', filter: 'Search',
attribute: 'Attribute', filterOperate: 'fitler operation',
old: 'Old', attribute: 'Attribute',
new: 'New', old: 'Old',
noUpdate: 'No update', new: 'New',
itemsPerPage: '/page', noUpdate: 'No update',
triggerName: 'Name', itemsPerPage: '/page',
event: 'Event', triggerName: 'Name',
action: 'Actoin', event: 'Event',
status: 'Status', action: 'Actoin',
done: 'Done', status: 'Status',
undone: 'Undone', done: 'Done',
triggerTime: 'Trigger Time', undone: 'Undone',
totalItems: '{total} records in total', triggerTime: 'Trigger Time',
pleaseSelect: 'Please select', totalItems: '{total} records in total',
startTime: 'Start Time', pleaseSelect: 'Please select',
endTime: 'End Time', startTime: 'Start Time',
deleteCIType: 'Delete CIType', endTime: 'End Time',
addCIType: 'Add CIType', deleteCIType: 'Delete CIType',
updateCIType: 'Update CIType', addCIType: 'Add CIType',
addAttribute: 'Add Attribute', updateCIType: 'Update CIType',
updateAttribute: 'Update Attribute', addAttribute: 'Add Attribute',
deleteAttribute: 'Delete Attribute', updateAttribute: 'Update Attribute',
addTrigger: 'Add Trigger', deleteAttribute: 'Delete Attribute',
updateTrigger: 'Update Trigger', addTrigger: 'Add Trigger',
deleteTrigger: 'Delete Trigger', updateTrigger: 'Update Trigger',
addUniqueConstraint: 'Add Unique Constraint', deleteTrigger: 'Delete Trigger',
updateUniqueConstraint: 'Update Unique Constraint', addUniqueConstraint: 'Add Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint', updateUniqueConstraint: 'Update Unique Constraint',
addRelation: 'Add Relation', deleteUniqueConstraint: 'Delete Unique Constraint',
deleteRelation: 'Delete Relation', addRelation: 'Add Relation',
noModifications: 'No Modifications', deleteRelation: 'Delete Relation',
attr: 'attribute', noModifications: 'No Modifications',
attrId: 'attribute id', attr: 'attribute',
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}' attrId: 'attribute id',
}, changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}'
relation_type: { },
addRelationType: 'New', relation_type: {
nameTips: 'Please enter a type name', addRelationType: 'New',
}, nameTips: 'Please enter a type name',
ad: { },
upload: 'Import', ad: {
download: 'Export', upload: 'Import',
accept: 'Accept', download: 'Export',
acceptBy: 'Accept By', accept: 'Accept',
acceptTime: 'Accept Time', acceptBy: 'Accept By',
confirmAccept: 'Confirm Accept?', acceptTime: 'Accept Time',
acceptSuccess: 'Accept successfully', confirmAccept: 'Confirm Accept?',
isAccept: 'Is accept', acceptSuccess: 'Accept successfully',
deleteADC: 'Confirm to delete this data?', isAccept: 'Is accept',
batchDelete: 'Confirm to delete this data?', deleteADC: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins', batchDelete: 'Confirm to delete this data?',
snmp: 'Network Devices', agent: 'Built-in & Plug-ins',
http: 'Public Clouds', snmp: 'Network Devices',
rule: 'AutoDiscovery Rules', http: 'Public Clouds',
timeout: 'Timeout error', rule: 'AutoDiscovery Rules',
mode: 'Mode', timeout: 'Timeout error',
collectSettings: 'Collection Settings', mode: 'Mode',
updateFields: 'Update Field', collectSettings: 'Collection Settings',
pluginScript: `# -*- coding:utf-8 -*- updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
import json
class AutoDiscovery(object):
class AutoDiscovery(object):
@property
def unique_key(self): @property
""" def unique_key(self):
"""
:return: Returns the name of a unique attribute
""" :return: Returns the name of a unique attribute
return """
return
@staticmethod
def attributes(): @staticmethod
""" def attributes():
Define attribute fields """
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. Define attribute fields
type: String Integer Float Date DateTime Time JSON :return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
For example: type: String Integer Float Date DateTime Time JSON
return [ For example:
("ci_type", "String", "CIType name"), return [
("private_ip", "String", "Internal IP, multiple values separated by commas") ("ci_type", "String", "CIType name"),
] ("private_ip", "String", "Internal IP, multiple values separated by commas")
""" ]
return [] """
return []
@staticmethod
def run(): @staticmethod
""" def run():
Execution entry, returns collected attribute values """
:return: Execution entry, returns collected attribute values
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value :return:
For example: Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
return [dict(ci_type="server", private_ip="192.168.1.1")] For example:
""" return [dict(ci_type="server", private_ip="192.168.1.1")]
return [] """
return []
if __name__ == "__main__":
result = AutoDiscovery().run() if __name__ == "__main__":
if isinstance(result, list): result = AutoDiscovery().run()
print("AutoDiscovery::Result::{}".format(json.dumps(result))) if isinstance(result, list):
else: print("AutoDiscovery::Result::{}".format(json.dumps(result)))
print("ERROR: The collection return must be a list") else:
`, print("ERROR: The collection return must be a list")
server: 'Server', `,
vserver: 'VServer', server: 'Server',
nic: 'NIC', vserver: 'VServer',
disk: 'harddisk', nic: 'NIC',
}, disk: 'harddisk',
ci: { },
attributeDesc: 'Attribute Description', ci: {
selectRows: 'Select: {rows} items', attributeDesc: 'Attribute Description',
addRelation: 'Add Relation', selectRows: 'Select: {rows} items',
all: 'All', addRelation: 'Add Relation',
batchUpdate: 'Batch Update', all: 'All',
batchUpdateConfirm: 'Are you sure you want to make batch updates?', batchUpdate: 'Batch Update',
batchUpdateInProgress: 'Currently being updated in batches', batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed', batchUpdateInProgress: 'Currently being updated in batches',
batchDeleting: 'Deleting...', batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed', batchDeleting: 'Deleting...',
copyFailed: 'Copy failed', batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
noLevel: 'No hierarchical relationship!', copyFailed: 'Copy failed',
batchAddRelation: 'Batch Add Relation', noLevel: 'No hierarchical relationship!',
history: 'History', batchAddRelation: 'Batch Add Relation',
topo: 'Topology', history: 'History',
table: 'Table', topo: 'Topology',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete', table: 'Table',
confirmDeleteRelation: 'Confirm to delete the relationship?', m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
tips1: 'Use commas to separate multiple values', confirmDeleteRelation: 'Confirm to delete the relationship?',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.', tips1: 'Use commas to separate multiple values',
tips3: 'Please select the fields that need to be modified', tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
tips4: 'At least one field must be selected', tips3: 'Please select the fields that need to be modified',
tips5: 'Search name | alias', tips4: 'At least one field must be selected',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json currently does not support indexing \n\nText characters longer than 190 cannot be indexed', tips5: 'Search name | alias',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value', tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips8: 'Multiple values, such as intranet IP', tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips9: 'For front-end only', tips8: 'Multiple values, such as intranet IP',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.', tips9: 'For front-end only',
newUpdateField: 'Add a Attribute', tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
attributeSettings: 'Attribute Settings', newUpdateField: 'Add a Attribute',
share: 'Share', attributeSettings: 'Attribute Settings',
noPermission: 'No Permission' share: 'Share',
}, noPermission: 'No Permission'
serviceTree: { },
deleteNode: 'Delete Node', serviceTree: {
tips1: 'For example: q=os_version:centos&sort=os_version', deleteNode: 'Delete Node',
tips2: 'Expression search', tips1: 'For example: q=os_version:centos&sort=os_version',
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!', tips2: 'Expression search',
copyFailed: 'Copy failed', alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?', copyFailed: 'Copy failed',
}, deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
tree: { batch: 'Batch',
tips1: 'Please go to Preference page first to complete your subscription!', grantTitle: 'Grant(read)',
subSettings: 'Settings', userPlaceholder: 'Please select users',
} rolePlaceholder: 'Please select roles',
} grantedByServiceTree: 'Granted By Service Tree:',
export default cmdb_en grantedByServiceTreeTips: 'Please delete id_filter in Servive Tree',
peopleHasRead: 'Personnel authorized to read:',
authorizationPolicy: 'CI Authorization Policy:',
idAuthorizationPolicy: 'Authorized by node:',
view: 'View permissions'
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
}
}
export default cmdb_en

View File

@ -1,491 +1,502 @@
const cmdb_zh = { const cmdb_zh = {
relation: '关系', relation: '关系',
attribute: '属性', attribute: '属性',
menu: { menu: {
views: '视图', views: '视图',
config: '配置', config: '配置',
backend: '管理端', backend: '管理端',
ciTable: '资源数据', ciTable: '资源数据',
ciTree: '资源层级', ciTree: '资源层级',
ciSearch: '资源搜索', ciSearch: '资源搜索',
adCIs: '自动发现池', adCIs: '自动发现池',
preference: '我的订阅', preference: '我的订阅',
batchUpload: '批量导入', batchUpload: '批量导入',
citypeManage: '模型配置', citypeManage: '模型配置',
backendManage: '后台管理', backendManage: '后台管理',
customDashboard: '定制仪表盘', customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义', serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系', citypeRelation: '模型关系',
operationHistory: '操作审计', operationHistory: '操作审计',
relationType: '关系类型', relationType: '关系类型',
ad: '自动发现', ad: '自动发现',
cidetail: 'CI 详情' cidetail: 'CI 详情'
}, },
ciType: { ciType: {
ciType: '模型', ciType: '模型',
attributes: '模型属性', attributes: '模型属性',
relation: '模型关联', relation: '模型关联',
trigger: '触发器', trigger: '触发器',
attributeAD: '属性自动发现', attributeAD: '属性自动发现',
relationAD: '关系自动发现', relationAD: '关系自动发现',
grant: '权限配置', grant: '权限配置',
addGroup: '新增分组', addGroup: '新增分组',
editGroup: '修改分组', editGroup: '修改分组',
group: '分组', group: '分组',
attributeLibray: '属性库', attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型', addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型', addCIType: '新增CI模型',
editGroupName: '编辑组名称', editGroupName: '编辑组名称',
deleteGroup: '删除该组', deleteGroup: '删除该组',
CITypeName: '模型名(英文)', CITypeName: '模型名(英文)',
English: '英文', English: '英文',
inputAttributeName: '请输入属性名', inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)', attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型', editCIType: '编辑模型',
defaultSort: '默认排序', defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性', selectDefaultOrderAttr: '选择默认排序属性',
asec: '正序', asec: '正序',
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
notfound: '找不到想要的?', uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', notfound: '找不到想要的?',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
uploading: '正在导入中', confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploadFailed: '导入失败,请稍后重试', uploading: '正在导入中',
addPlugin: '新建plugin', uploadFailed: '导入失败,请稍后重试',
deletePlugin: '删除plugin', addPlugin: '新建plugin',
confirmDeleteADT: '确认删除 【{pluginName}】', deletePlugin: '删除plugin',
attributeMap: '字段映射', confirmDeleteADT: '确认删除 【{pluginName}】',
autoDiscovery: '自动发现', attributeMap: '字段映射',
node: '节点', autoDiscovery: '自动发现',
adExecConfig: '执行配置', node: '节点',
adExecTarget: '执行机器', adExecConfig: '执行配置',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID', adExecTarget: '执行机器',
selectFromCMDBTips: '从CMDB中选择 ', oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
adAutoInLib: '自动入库', selectFromCMDBTips: '从CMDB中选择 ',
adInterval: '采集频率', adAutoInLib: '自动入库',
byInterval: '按间隔', adInterval: '采集频率',
allNodes: '所有节点', byInterval: '按间隔',
specifyNodes: '指定节点', allNodes: '所有节点',
specifyNodesTips: '请填写指定节点!', specifyNodes: '指定节点',
username: '用户名', specifyNodesTips: '请填写指定节点!',
password: '密码', username: '用户名',
link: '链接', password: '密码',
list: '多值', link: '链接',
listTips: '字段的值是1个或者多个接口返回的值的类型是list', list: '多值',
computeForAllCITips: '所有CI触发计算', listTips: '字段的值是1个或者多个接口返回的值的类型是list',
confirmcomputeForAllCITips: '确认触发所有CI的计算', computeForAllCITips: '所有CI触发计算',
isUnique: '是否唯一', confirmcomputeForAllCITips: '确认触发所有CI的计算',
unique: '唯一', isUnique: '是否唯一',
isChoice: '是否选择', unique: '唯一',
defaultShow: '默认显示', isChoice: '是否选择',
defaultShowTips: 'CI实例表格默认展示该字段', defaultShow: '默认显示',
isSortable: '可排序', defaultShowTips: 'CI实例表格默认展示该字段',
isIndex: '是否索引', isSortable: '可排序',
index: '索引', isIndex: '是否索引',
indexTips: '字段可被用于检索,加速查询', index: '索引',
confirmDelete: '确认删除【{name}】?', indexTips: '字段可被用于检索,加速查询',
confirmDelete2: '确认删除?', confirmDelete: '确认删除【{name}】?',
computeSuccess: '触发成功!', confirmDelete2: '确认删除?',
basicConfig: '基础设置', computeSuccess: '触发成功!',
AttributeName: '属性名(英文)', basicConfig: '基础设置',
DataType: '数据类型', AttributeName: '属性名(英文)',
defaultValue: '默认值', DataType: '数据类型',
autoIncID: '自增ID', defaultValue: '默认值',
customTime: '自定义时间', autoIncID: '自增ID',
advancedSettings: '高级设置', customTime: '自定义时间',
font: '字体', advancedSettings: '高级设置',
color: '颜色', font: '字体',
choiceValue: '预定义值', color: '颜色',
computedAttribute: '计算属性', choiceValue: '预定义值',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}', computedAttribute: '计算属性',
addAttribute: '新增属性', computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
existedAttributes: '已有属性', addAttribute: '新增属性',
editAttribute: '编辑属性', existedAttributes: '已有属性',
addAttributeTips1: '选中排序,则必须也要选中!', editAttribute: '编辑属性',
uniqueConstraint: '唯一校验', addAttributeTips1: '选中排序,则必须也要选中!',
up: '上移', uniqueConstraint: '唯一校验',
down: '下移', up: '上移',
selectAttribute: '添加属性', down: '下移',
groupExisted: '分组名称已存在', selectAttribute: '添加属性',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!', groupExisted: '分组名称已存在',
buildinAttribute: '内置字段', attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
expr: '表达式', buildinAttribute: '内置字段',
code: '代码', expr: '表达式',
apply: '应用', code: '代码',
continueAdd: '继续添加', apply: '应用',
filter: '过滤', continueAdd: '继续添加',
choiceOther: '其他模型属性', filter: '过滤',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]', choiceOther: '其他模型属性',
selectCIType: '请选择CMDB模型', choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCITypeAttributes: '请选择模型属性', selectCIType: '请选择CMDB模型',
selectAttributes: '请选择属性', selectCITypeAttributes: '请选择模型属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []', selectAttributes: '请选择属性',
valueExisted: '当前值已存在!', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
addRelation: '新增关系', valueExisted: '当前值已存在!',
sourceCIType: '源模型', addRelation: '新增关系',
sourceCITypeTips: '请选择源模型', sourceCIType: '源模型',
dstCIType: '目标模型名', sourceCITypeTips: '请选择源模型',
dstCITypeTips: '请选择目标模型', dstCIType: '目标模型名',
relationType: '关联类型', dstCITypeTips: '请选择目标模型',
relationTypeTips: '请选择关联类型', relationType: '关联类型',
isParent: '被', relationTypeTips: '请选择关联类型',
relationConstraint: '关系约束', isParent: '被',
relationConstraintTips: '请选择关系约束', relationConstraint: '关系约束',
one2Many: '一对多', relationConstraintTips: '请选择关系约束',
one2One: '一对一', one2Many: '一对多',
many2Many: '多对多', one2One: '一对一',
basicInfo: '基本信息', many2Many: '多对多',
nameInputTips: '请输入名称', basicInfo: '基本信息',
triggerDataChange: '数据变更', nameInputTips: '请输入名称',
triggerDate: '日期属性', triggerDataChange: '数据变更',
triggerEnable: '开启', triggerDate: '日期属性',
descInput: '请输入备注', triggerEnable: '开启',
triggerCondition: '触发条件', descInput: '请输入备注',
addInstance: '新增实例', triggerCondition: '触发条件',
deleteInstance: '删除实例', addInstance: '新增实例',
changeInstance: '实例变更', deleteInstance: '删除实例',
selectMutipleAttributes: '请选择属性(多选)', changeInstance: '实例变更',
selectSingleAttribute: '请选择属性(单选)', selectMutipleAttributes: '请选择属性(多选)',
beforeDays: '提前', selectSingleAttribute: '请选择属性(单选)',
days: '天', beforeDays: '提前',
notifyAt: '发送时间', days: '天',
notify: '通知', notifyAt: '发送时间',
triggerAction: '触发动作', notify: '通知',
receivers: '收件人', triggerAction: '触发动作',
emailTips: '请输入邮箱,多个邮箱用;分隔', receivers: '收件人',
customEmail: '自定义收件人', emailTips: '请输入邮箱,多个邮箱用;分隔',
notifySubject: '通知标题', customEmail: '自定义收件人',
notifySubjectTips: '请输入通知标题', notifySubject: '通知标题',
notifyContent: '内容', notifySubjectTips: '请输入通知标题',
notifyMethod: '通知方式', notifyContent: '内容',
botSelect: '请选择机器人', notifyMethod: '通知方式',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}', botSelect: '请选择机器人',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}', refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器', webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
editTriggerTitle: '编辑触发器 {name}', newTrigger: '新增触发器',
newTriggerTitle: '新增触发器 {name}', editTriggerTitle: '编辑触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?', newTriggerTitle: '新增触发器 {name}',
int: '整数', confirmDeleteTrigger: '确认删除该触发器吗?',
float: '浮点数', int: '整数',
text: '文本', float: '浮点数',
datetime: '日期时间', text: '文本',
date: '日期', datetime: '日期时间',
time: '时间', date: '日期',
json: 'JSON', time: '时间',
event: '事件', json: 'JSON',
reg: '正则校验', event: '事件',
isInherit: '是否继承', reg: '正则校验',
inheritType: '继承模型', isInherit: '是否继承',
inheritTypePlaceholder: '请选择继承模型(多选)', inheritType: '继承模型',
inheritFrom: '属性继承自{name}', inheritTypePlaceholder: '请选择继承模型(多选)',
groupInheritFrom: '请至{name}进行修改' inheritFrom: '属性继承自{name}',
}, groupInheritFrom: '请至{name}进行修改'
components: { },
unselectAttributes: '未选属性', components: {
selectAttributes: '已选属性', unselectAttributes: '未选属性',
downloadCI: '导出数据', selectAttributes: '已选属性',
filename: '文件名', downloadCI: '导出数据',
filenameInputTips: '请输入文件名', filename: '文件名',
saveType: '保存类型', filenameInputTips: '请输入文件名',
saveTypeTips: '请选择保存类型', saveType: '保存类型',
xlsx: 'Excel工作簿(*.xlsx)', saveTypeTips: '请选择保存类型',
csv: 'CSV(逗号分隔)(*.csv)', xlsx: 'Excel工作簿(*.xlsx)',
html: '网页(*.html)', csv: 'CSV(逗号分隔)(*.csv)',
xml: 'XML数据(*.xml)', html: '网页(*.html)',
txt: '文本文件(制表符分隔)(*.txt)', xml: 'XML数据(*.xml)',
grantUser: '授权用户/部门', txt: '文本文件(制表符分隔)(*.txt)',
grantRole: '授权角色', grantUser: '授权用户/部门',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?', grantRole: '授权角色',
readAttribute: '查看字段', confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?',
readCI: '查看实例', readAttribute: '查看字段',
config: '配置', readCI: '查看实例',
ciTypeGrant: '模型权限', config: '配置',
ciGrant: '实例权限', ciTypeGrant: '模型权限',
attributeGrant: '字段权限', ciGrant: '实例权限',
relationGrant: '关系权限', attributeGrant: '字段权限',
perm: '权限', relationGrant: '关系权限',
all: '全部', perm: '权限',
customize: '自定义', all: '全部',
none: '无', customize: '自定义',
customizeFilterName: '请输入自定义筛选条件名', none: '无',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式', customizeFilterName: '请输入自定义筛选条件名',
example: '示例值', colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
aliyun: '阿里云', example: '示例值',
tencentcloud: '腾讯云', aliyun: '阿里云',
huaweicloud: '华为云', tencentcloud: '腾讯云',
beforeChange: '变更前', huaweicloud: '华为云',
afterChange: '变更后', beforeChange: '变更前',
noticeContentTips: '请输入通知内容', afterChange: '变更后',
saveQuery: '保存筛选条件', noticeContentTips: '请输入通知内容',
pleaseSearch: '请查找', saveQuery: '保存筛选条件',
conditionFilter: '条件过滤', pleaseSearch: '请查找',
attributeDesc: '属性说明', conditionFilter: '条件过滤',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤', attributeDesc: '属性说明',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
subCIType: '订阅模型', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
already: '已', subCIType: '订阅模型',
not: '未', already: '已',
sub: '订阅', not: '未',
selectBelow: '请在下方进行选择', sub: '订阅',
subSuccess: '订阅成功', selectBelow: '请在下方进行选择',
selectMethods: '请选择方式', subSuccess: '订阅成功',
noAuthRequest: '暂无请求认证', selectMethods: '请选择方式',
noParamRequest: '暂无参数认证', noAuthRequest: '暂无请求认证',
requestParam: '请求参数', noParamRequest: '暂无参数认证',
param: '参数{param}', requestParam: '请求参数',
value: '值{value}', param: '参数{param}',
clear: '清空', value: '值{value}',
}, clear: '清空',
batch: { },
downloadFailed: '失败下载', batch: {
unselectCIType: '尚未选择模板类型', downloadFailed: '失败下载',
pleaseUploadFile: '请上传文件', unselectCIType: '尚未选择模板类型',
batchUploadCanceled: '批量上传已取消', pleaseUploadFile: '请上传文件',
selectCITypeTips: '请选择模板类型', batchUploadCanceled: '批量上传已取消',
downloadTemplate: '下载模板', selectCITypeTips: '请选择模板类型',
drawTips: '点击或拖拽文件至此上传!', downloadTemplate: '下载模板',
supportFileTypes: '支持文件类型xlsxlsx', drawTips: '点击或拖拽文件至此上传!',
uploadResult: '上传结果', supportFileTypes: '支持文件类型xlsxlsx',
total: '共', uploadResult: '上传结果',
successItems: '条,已成功', total: '共',
failedItems: '条,失败', successItems: '条,已成功',
items: '条', failedItems: '条,失败',
errorTips: '错误信息', items: '条',
requestFailedTips: '请求出现错误,请稍后再试', errorTips: '错误信息',
requestSuccessTips: '批量上传已完成', requestFailedTips: '请求出现错误,请稍后再试',
}, requestSuccessTips: '批量上传已完成',
preference: { },
mySub: '我的订阅', preference: {
sub: '订阅', mySub: '我的订阅',
cancelSub: '取消订阅', sub: '订阅',
editSub: '编辑订阅', cancelSub: '取消订阅',
peopleSub: '位同事已订阅', editSub: '编辑订阅',
noSub: '暂无同事订阅', peopleSub: '位同事已订阅',
cancelSubSuccess: '取消订阅成功', noSub: '暂无同事订阅',
confirmcancelSub: '确认取消订阅', cancelSubSuccess: '取消订阅成功',
confirmcancelSub2: '确认取消订阅 {name} 吗?', confirmcancelSub: '确认取消订阅',
of: '的', confirmcancelSub2: '确认取消订阅 {name} 吗?',
hoursAgo: '小时前', of: '的',
daysAgo: '天前', hoursAgo: '小时前',
monthsAgo: '月前', daysAgo: '天前',
yearsAgo: '年前', monthsAgo: '月前',
just: '刚刚', yearsAgo: '年前',
}, just: '刚刚',
custom_dashboard: { },
charts: '图表', custom_dashboard: {
newChart: '新增图表', charts: '图表',
editChart: '编辑图表', newChart: '新增图表',
title: '标题', editChart: '编辑图表',
titleTips: '请输入图表标题', title: '标题',
calcIndicators: '计算指标', titleTips: '请输入图表标题',
dimensions: '维度', calcIndicators: '计算指标',
selectDimensions: '请选择维度', dimensions: '维度',
quantity: '数量', selectDimensions: '请选择维度',
childCIType: '关系模型', quantity: '数量',
level: '层级', childCIType: '关系模型',
levelTips: '请输入关系层级', level: '层级',
preview: '预览', levelTips: '请输入关系层级',
showIcon: '是否显示icon', preview: '预览',
chartType: '图表类型', showIcon: '是否显示icon',
dataFilter: '数据筛选', chartType: '图表类型',
format: '格式', dataFilter: '数据筛选',
fontColor: '字体颜色', format: '格式',
backgroundColor: '背景颜色', fontColor: '字体颜色',
chartColor: '图表颜色', backgroundColor: '背景颜色',
chartLength: '图表长度', chartColor: '图表颜色',
barType: '柱状图类型', chartLength: '图表长度',
stackedBar: '堆积柱状图', barType: '柱状图类型',
multipleSeriesBar: '多系列柱状图', stackedBar: '堆积柱状图',
axis: '轴', multipleSeriesBar: '多系列柱状图',
direction: '方向', axis: '轴',
lowerShadow: '下方阴影', direction: '方向',
count: '指标', lowerShadow: '下方阴影',
bar: '柱状图', count: '指标',
line: '折线图', bar: '柱状图',
pie: '饼状图', line: '折线图',
table: '表格', pie: '饼状图',
default: '默认', table: '表格',
relation: '关系', default: '默认',
noCustomDashboard: '管理员暂未定制仪表盘', relation: '关系',
}, noCustomDashboard: '管理员暂未定制仪表盘',
preference_relation: { },
newServiceTree: '新增服务树', preference_relation: {
serviceTreeName: '服务树名', newServiceTree: '新增服务树',
public: '公开', serviceTreeName: '服务树名',
saveLayout: '保存布局', public: '公开',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!', saveLayout: '保存布局',
tips1: '不能与当前选中节点形成视图,请重新选择!', childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips2: '请输入新增服务树名!', tips1: '不能与当前选中节点形成视图,请重新选择!',
tips3: '请选择至少两个节点!', tips2: '请输入新增服务树名!',
}, tips3: '请选择至少两个节点!',
history: { },
ciChange: 'CI变更', history: {
relationChange: '关系变更', ciChange: 'CI变更',
ciTypeChange: '模型变更', relationChange: '关系变更',
triggerHistory: '触发历史', ciTypeChange: '模型变更',
opreateTime: '操作时间', triggerHistory: '触发历史',
user: '用户', opreateTime: '操作时间',
userTips: '输入筛选用户名', user: '用户',
filter: '筛选', userTips: '输入筛选用户名',
filterOperate: '筛选操作', filter: '筛选',
attribute: '属性', filterOperate: '筛选操作',
old: '旧', attribute: '属性',
new: '新', old: '旧',
noUpdate: '没有修改', new: '新',
itemsPerPage: '/页', noUpdate: '没有修改',
triggerName: '触发器名称', itemsPerPage: '/页',
event: '事件', triggerName: '触发器名称',
action: '动作', event: '事件',
status: '状态', action: '动作',
done: '已完成', status: '状态',
undone: '未完成', done: '已完成',
triggerTime: '触发时间', undone: '未完成',
totalItems: '共 {total} 条记录', triggerTime: '触发时间',
pleaseSelect: '请选择', totalItems: '共 {total} 条记录',
startTime: '开始时间', pleaseSelect: '请选择',
endTime: '结束时间', startTime: '开始时间',
deleteCIType: '删除模型', endTime: '结束时间',
addCIType: '新增模型', deleteCIType: '删除模型',
updateCIType: '修改模型', addCIType: '新增模型',
addAttribute: '新增属性', updateCIType: '修改模型',
updateAttribute: '修改属性', addAttribute: '新增属性',
deleteAttribute: '删除属性', updateAttribute: '修改属性',
addTrigger: '新增触发器', deleteAttribute: '删除属性',
updateTrigger: '修改触发器', addTrigger: '新增触发器',
deleteTrigger: '删除触发器', updateTrigger: '修改触发器',
addUniqueConstraint: '新增联合唯一', deleteTrigger: '删除触发器',
updateUniqueConstraint: '修改联合唯一', addUniqueConstraint: '新增联合唯一',
deleteUniqueConstraint: '删除联合唯一', updateUniqueConstraint: '修改联合唯一',
addRelation: '新增关系', deleteUniqueConstraint: '删除联合唯一',
deleteRelation: '删除关系', addRelation: '新增关系',
noModifications: '没有修改', deleteRelation: '删除关系',
attr: '属性名', noModifications: '没有修改',
attrId: '属性ID', attr: '属性名',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}' attrId: '属性ID',
}, changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
relation_type: { },
addRelationType: '新增关系类型', relation_type: {
nameTips: '请输入类型名', addRelationType: '新增关系类型',
}, nameTips: '请输入类型名',
ad: { },
upload: '规则导入', ad: {
download: '规则导出', upload: '规则导入',
accept: '入库', download: '规则导出',
acceptBy: '入库人', accept: '入库',
acceptTime: '入库时间', acceptBy: '入库人',
confirmAccept: '确认入库?', acceptTime: '入库时间',
acceptSuccess: '入库成功', confirmAccept: '确认入库?',
isAccept: '是否入库', acceptSuccess: '入库成功',
deleteADC: '确认删除该条数据?', isAccept: '是否入库',
batchDelete: '确认删除这些数据?', deleteADC: '确认删除该条数据?',
agent: '内置 & 插件', batchDelete: '确认删除这些数据?',
snmp: '网络设备', agent: '内置 & 插件',
http: '公有云资源', snmp: '网络设备',
rule: '自动发现规则', http: '公有云资源',
timeout: '超时错误', rule: '自动发现规则',
mode: '模式', timeout: '超时错误',
collectSettings: '采集设置', mode: '模式',
updateFields: '更新字段', collectSettings: '采集设置',
pluginScript: `# -*- coding:utf-8 -*- updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
import json
class AutoDiscovery(object):
class AutoDiscovery(object):
@property
def unique_key(self): @property
""" def unique_key(self):
"""
:return: 返回唯一属性的名字
""" :return: 返回唯一属性的名字
return """
return
@staticmethod
def attributes(): @staticmethod
""" def attributes():
定义属性字段 """
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文 定义属性字段
类型: String Integer Float Date DateTime Time JSON :return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
例如: 类型: String Integer Float Date DateTime Time JSON
return [ 例如:
("ci_type", "String", "模型名称"), return [
("private_ip", "String", "内网IP, 多值逗号分隔") ("ci_type", "String", "模型名称"),
] ("private_ip", "String", "内网IP, 多值逗号分隔")
""" ]
return [] """
return []
@staticmethod
def run(): @staticmethod
""" def run():
执行入口, 返回采集的属性值 """
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值 执行入口, 返回采集的属性值
例如: :return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
return [dict(ci_type="server", private_ip="192.168.1.1")] 例如:
""" return [dict(ci_type="server", private_ip="192.168.1.1")]
return [] """
return []
if __name__ == "__main__":
result = AutoDiscovery().run() if __name__ == "__main__":
if isinstance(result, list): result = AutoDiscovery().run()
print("AutoDiscovery::Result::{}".format(json.dumps(result))) if isinstance(result, list):
else: print("AutoDiscovery::Result::{}".format(json.dumps(result)))
print("ERROR: 采集返回必须是列表") else:
`, print("ERROR: 采集返回必须是列表")
server: '物理机', `,
vserver: '虚拟机', server: '物理机',
nic: '网卡', vserver: '虚拟机',
disk: '硬盘', nic: '网卡',
}, disk: '硬盘',
ci: { },
attributeDesc: '属性说明', ci: {
selectRows: '选取:{rows} 项', attributeDesc: '属性说明',
addRelation: '添加关系', selectRows: '选取:{rows} 项',
all: '全部', addRelation: '添加关系',
batchUpdate: '批量修改', all: '全部',
batchUpdateConfirm: '确认要批量修改吗?', batchUpdate: '批量修改',
batchUpdateInProgress: '正在批量修改', batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个', batchUpdateInProgress: '正在批量修改',
batchDeleting: '正在删除...', batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个', batchDeleting: '正在删除...',
copyFailed: '复制失败!', batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
noLevel: '无层级关系!', copyFailed: '复制失败!',
batchAddRelation: '批量添加关系', noLevel: '无层级关系!',
history: '操作历史', batchAddRelation: '批量添加关系',
topo: '拓扑', history: '操作历史',
table: '表格', topo: '拓扑',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作', table: '表格',
confirmDeleteRelation: '确认删除关系?', m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
tips1: '多个值使用,分割', confirmDeleteRelation: '确认删除关系?',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空', tips1: '多个值使用,分割',
tips3: '请选择需要修改的字段', tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips4: '必须至少选择一个字段', tips3: '请选择需要修改的字段',
tips5: '搜索 名称 | 别名', tips4: '必须至少选择一个字段',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips5: '搜索 名称 | 别名',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips8: '多值, 比如内网IP', tips7: '表现形式是下拉框, 值必须在预定义值里',
tips9: '仅针对前端', tips8: '多值, 比如内网IP',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值', tips9: '仅针对前端',
newUpdateField: '新增修改字段', tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
attributeSettings: '字段设置', newUpdateField: '新增修改字段',
share: '分享', attributeSettings: '字段设置',
noPermission: '暂无权限' share: '分享',
}, noPermission: '暂无权限'
serviceTree: { },
deleteNode: '删除节点', serviceTree: {
tips1: '例q=os_version:centos&sort=os_version', deleteNode: '删除节点',
tips2: '表达式搜索', tips1: '例q=os_version:centos&sort=os_version',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!', tips2: '表达式搜索',
copyFailed: '复制失败', alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?', copyFailed: '复制失败',
}, deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
tree: { batch: '批量操作',
tips1: '请先到 我的订阅 页面完成订阅!', grantTitle: '授权(查看权限)',
subSettings: '订阅设置', userPlaceholder: '请选择用户',
} rolePlaceholder: '请选择角色',
} grantedByServiceTree: '服务树授权:',
export default cmdb_zh grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh

View File

@ -1,394 +1,430 @@
<template> <template>
<CustomDrawer <CustomDrawer
:title="title + CIType.alias" :title="title + CIType.alias"
width="800" width="800"
@close="handleClose" @close="handleClose"
:maskClosable="false" :maskClosable="false"
:visible="visible" :visible="visible"
wrapClassName="create-instance-form" wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }" :bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }" :headerStyle="{ borderBottom: 'none' }"
> >
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button> <a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button> <a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div> </div>
<template v-if="action === 'create'"> <template v-if="action === 'create'">
<template v-for="group in attributesByGroup"> <template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup <CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`" :ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name" :key="group.id || group.name"
:group="group" :group="group"
@handleFocusInput="handleFocusInput" @handleFocusInput="handleFocusInput"
:attributeList="attributeList" :attributeList="attributeList"
/> />
</template> </template>
<template v-if="parentsType && parentsType.length"> <template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{ <a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation') $t('cmdb.menu.citypeRelation')
}}</a-divider> }}</a-divider>
<a-form> <a-form>
<a-row :gutter="24" align="top" type="flex"> <a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id"> <a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false"> <a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%"> <a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr"> <a-select v-model="parentsForm[item.name].attr">
<a-select-option <a-select-option
:title="attr.alias || attr.name" :title="attr.alias || attr.name"
v-for="attr in item.attributes" v-for="attr in item.attributes"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input <a-input
:placeholder="$t('cmdb.ci.tips1')" :placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value" v-model="parentsForm[item.name].value"
style="width: 50%" style="width: 50%"
/> />
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</template> </template>
</template> </template>
<template v-if="action === 'update'"> <template v-if="action === 'update'">
<a-form :form="form"> <a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p> <p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name"> <a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')"> <el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option <el-option
v-for="attr in attributeList" v-for="attr in attributeList"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1" :disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name" :label="attr.alias || attr.name"
> >
</el-option> </el-option>
</el-select> </el-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'" v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'" :mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
> >
<a-select-option <a-select-option
:value="choice[0]" :value="choice[0]"
:key="'New_' + choice + choice_idx" :key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)" v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
> >
<span :style="choice[1] ? choice[1].style || {} : {}"> <span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon <ops-icon
:style="{ color: choice[1].icon.color }" :style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name" v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name" :type="choice[1].icon.name"
/> />
{{ choice[0] }} {{ choice[0] }}
</span> </span>
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input-number <a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'" v-if="getFieldType(list.name) === 'input_number'"
/> />
<a-date-picker <a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" :format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'" v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }" :showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/> />
<a-input <a-input
v-if="getFieldType(list.name) === 'input'" v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)" @focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="2"> <a-col :span="2">
<a-form-item> <a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)"> <a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" /> <a-icon type="delete" />
</a> </a>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button> <a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form> </a-form>
</template> </template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer> </CustomDrawer>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import { Select, Option } from 'element-ui' import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType' import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci' import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const' import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation' import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default { export default {
name: 'CreateInstanceForm', name: 'CreateInstanceForm',
components: { components: {
ElSelect: Select, ElSelect: Select,
ElOption: Option, ElOption: Option,
JsonEditor, JsonEditor,
CreateInstanceFormByGroup, CreateInstanceFormByGroup,
}, },
props: { props: {
typeIdFromRelation: { typeIdFromRelation: {
type: Number, type: Number,
default: 0, default: 0,
}, },
}, },
data() { data() {
return { return {
action: '', action: '',
form: this.$form.createForm(this), form: this.$form.createForm(this),
visible: false, visible: false,
attributeList: [], attributeList: [],
CIType: {}, CIType: {},
batchUpdateLists: [], batchUpdateLists: [],
editAttr: null, editAttr: null,
attributesByGroup: [], attributesByGroup: [],
parentsType: [], parentsType: [],
parentsForm: {}, parentsForm: {},
canEdit: {}, canEdit: {},
} }
}, },
computed: { computed: {
title() { title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' ' return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
}, },
typeId() { typeId() {
if (this.typeIdFromRelation) { if (this.typeIdFromRelation) {
return this.typeIdFromRelation return this.typeIdFromRelation
} }
return this.$router.currentRoute.meta.typeId return this.$router.currentRoute.meta.typeId
}, },
valueTypeMap() { valueTypeMap() {
return valueTypeMap() return valueTypeMap()
}, },
}, },
provide() { provide() {
return { return {
getFieldType: this.getFieldType, getFieldType: this.getFieldType,
} }
}, },
inject: ['attrList'], inject: ['attrList'],
methods: { methods: {
moment, moment,
async getCIType() { async getCIType() {
await getCIType(this.typeId).then((res) => { await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0] this.CIType = res.ci_types[0]
}) })
}, },
async getAttributeList() { async getAttributeList() {
const _attrList = this.attrList() const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required) this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => { await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => { const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed) g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g return g
}) })
const attrHasGroupIds = [] const attrHasGroupIds = []
res1.forEach((g) => { res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id) const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id) attrHasGroupIds.push(...id)
}) })
const otherGroupAttr = this.attributeList.filter( const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed (attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
) )
if (otherGroupAttr.length) { if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr }) _attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
} }
this.attributesByGroup = _attributesByGroup console.log(otherGroupAttr, _attributesByGroup)
}) this.attributesByGroup = _attributesByGroup
}, })
createInstance() { },
const _this = this createInstance() {
if (_this.action === 'update') { const _this = this
this.form.validateFields((err, values) => { if (_this.action === 'update') {
if (err) { this.form.validateFields((err, values) => {
return if (err) {
} return
Object.keys(values).forEach((k) => { }
const _tempFind = this.attributeList.find((item) => item.name === k) Object.keys(values).forEach((k) => {
if ( const _tempFind = this.attributeList.find((item) => item.name === k)
_tempFind.value_type === '3' && if (
values[k] && _tempFind.value_type === '3' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
if ( }
_tempFind.value_type === '4' && if (
values[k] && _tempFind.value_type === '4' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD') ) {
} values[k] = values[k].format('YYYY-MM-DD')
if (_tempFind.value_type === '6') { }
values[k] = values[k] ? JSON.parse(values[k]) : undefined if (_tempFind.value_type === '6') {
} values[k] = values[k] ? JSON.parse(values[k]) : undefined
}) }
})
_this.$emit('submit', values)
}) _this.$emit('submit', values)
} else { })
let values = {} } else {
for (let i = 0; i < this.attributesByGroup.length; i++) { let values = {}
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData() for (let i = 0; i < this.attributesByGroup.length; i++) {
if (data === 'error') { const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
return if (data === 'error') {
} return
values = { ...values, ...data } }
} values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k) Object.keys(values).forEach((k) => {
if ( const _tempFind = this.attributeList.find((item) => item.name === k)
_tempFind.value_type === '3' && if (
values[k] && _tempFind.value_type === '3' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
if ( }
_tempFind.value_type === '4' && if (
values[k] && _tempFind.value_type === '4' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD') ) {
} values[k] = values[k].format('YYYY-MM-DD')
if (_tempFind.value_type === '6') { }
values[k] = values[k] ? JSON.parse(values[k]) : undefined if (_tempFind.value_type === '6') {
} values[k] = values[k] ? JSON.parse(values[k]) : undefined
}) }
values.ci_type = _this.typeId })
console.log(this.parentsForm) values.ci_type = _this.typeId
Object.keys(this.parentsForm).forEach((type) => { console.log(this.parentsForm)
if (this.parentsForm[type].value) { Object.keys(this.parentsForm).forEach((type) => {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value if (this.parentsForm[type].value) {
} values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}) }
addCI(values).then((res) => { })
_this.$message.success(this.$t('addSuccess')) addCI(values).then((res) => {
_this.visible = false _this.$message.success(this.$t('addSuccess'))
_this.$emit('reload', { ci_id: res.ci_id }) _this.visible = false
}) _this.$emit('reload', { ci_id: res.ci_id })
} })
}, }
handleClose() {
this.visible = false // this.form.validateFields((err, values) => {
}, // if (err) {
handleOpen(visible, action) { // _this.$message.error('字段填写不符合要求!')
this.visible = visible // return
this.action = action // }
this.$nextTick(() => { // Object.keys(values).forEach((k) => {
this.form.resetFields() // if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => { // values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
this.batchUpdateLists = [{ name: this.attributeList[0].name }] // }
}) // const _tempFind = this.attributeList.find((item) => item.name === k)
if (action === 'create') { // if (_tempFind.value_type === '6') {
getCITypeParent(this.typeId).then(async (res) => { // values[k] = values[k] ? JSON.parse(values[k]) : undefined
for (let i = 0; i < res.parents.length; i++) { // }
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => { // })
this.canEdit = {
..._.cloneDeep(this.canEdit), // if (_this.action === 'update') {
[res.parents[i].id]: p_res.result, // _this.$emit('submit', values)
} // return
}) // }
} // values.ci_type = _this.typeId
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id]) // console.log(values)
const _parentsForm = {} // this.attributesByGroup.forEach((group) => {
res.parents.forEach((item) => { // this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
const _find = item.attributes.find((attr) => attr.id === item.unique_id) // })
_parentsForm[item.name] = { attr: _find.name, value: '' } // console.log(1111)
}) // // addCI(values).then((res) => {
this.parentsForm = _parentsForm // // _this.$message.success('新增成功!')
}) // // _this.visible = false
} // // _this.$emit('reload')
}) // // })
}, // })
getFieldType(name) { },
const _find = this.attributeList.find((item) => item.name === name) handleClose() {
if (_find) { this.visible = false
if (_find.is_choice) { },
if (_find.is_list) { handleOpen(visible, action) {
return 'select%%multiple' this.visible = visible
} this.action = action
return 'select' this.$nextTick(() => {
} else if (_find.value_type === '0' || _find.value_type === '1') { this.form.resetFields()
return 'input_number' Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
} else if (_find.value_type === '4' || _find.value_type === '3') { this.batchUpdateLists = [{ name: this.attributeList[0].name }]
return this.valueTypeMap[_find.value_type] })
} else { if (action === 'create') {
return 'input' getCITypeParent(this.typeId).then(async (res) => {
} for (let i = 0; i < res.parents.length; i++) {
} await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
return 'input' this.canEdit = {
}, ..._.cloneDeep(this.canEdit),
getSelectFieldOptions(name) { [res.parents[i].id]: p_res.result,
const _find = this.attributeList.find((item) => item.name === name) }
if (_find) { })
return _find.choice_value }
} this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
return [] const _parentsForm = {}
}, res.parents.forEach((item) => {
handleAdd() { const _find = item.attributes.find((attr) => attr.id === item.unique_id)
this.batchUpdateLists.push({ name: undefined }) _parentsForm[item.name] = { attr: _find.name, value: '' }
}, })
handleDelete(name) { this.parentsForm = _parentsForm
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name) })
if (_idx > -1) { }
this.batchUpdateLists.splice(_idx, 1) })
} },
}, getFieldType(name) {
handleFocusInput(e, attr) { const _find = this.attributeList.find((item) => item.name === name)
console.log(attr) if (_find) {
const _tempFind = this.attributeList.find((item) => item.name === attr.name) if (_find.is_choice) {
if (_tempFind.value_type === '6') { if (_find.is_list) {
this.editAttr = attr return 'select%%multiple'
e.srcElement.blur() }
const jsonData = this.form.getFieldValue(attr.name) return 'select'
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {}) } else if (_find.value_type === '0' || _find.value_type === '1') {
} else { return 'input_number'
this.editAttr = null } else if (_find.value_type === '4' || _find.value_type === '3') {
} return this.valueTypeMap[_find.value_type]
}, } else {
jsonEditorOk(jsonData) { return 'input'
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) }) }
}, }
}, return 'input'
} },
</script> getSelectFieldOptions(name) {
<style lang="less"> const _find = this.attributeList.find((item) => item.name === name)
.create-instance-form { if (_find) {
.ant-form-item { return _find.choice_value
margin-bottom: 5px; }
} return []
.ant-drawer-body { },
overflow-y: auto; handleAdd() {
max-height: calc(100vh - 110px); this.batchUpdateLists.push({ name: undefined })
} },
} handleDelete(name) {
</style> const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>

View File

@ -1,390 +1,390 @@
<template> <template>
<div :style="{ height: '100%' }"> <div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab"> <a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }"> <a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" /> <a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }} {{ $t('cmdb.ci.share') }}
</a> </a>
<a-tab-pane key="tab_1"> <a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span> <span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr"> <div class="ci-detail-attr">
<el-descriptions <el-descriptions
:title="group.name || $t('other')" :title="group.name || $t('other')"
:key="group.name" :key="group.name"
v-for="group in attributeGroups" v-for="group in attributeGroups"
border border
:column="3" :column="3"
> >
<el-descriptions-item <el-descriptions-item
:label="`${attr.alias || attr.name}`" :label="`${attr.alias || attr.name}`"
:key="attr.name" :key="attr.name"
v-for="attr in group.attributes" v-for="attr in group.attributes"
> >
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" /> <CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_2"> <a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span> <span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px' }"> <div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" /> <CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_3"> <a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span> <span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '24px', height: '100%' }">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
:data="ciHistory" :data="ciHistory"
size="small" size="small"
height="auto" height="auto"
:span-method="mergeRowMethod" :span-method="mergeRowMethod"
border border
:scroll-y="{ enabled: false }" :scroll-y="{ enabled: false }"
class="ops-stripe-table" class="ops-stripe-table"
> >
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column> <vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column <vxe-table-column
field="username" field="username"
:title="$t('user')" :title="$t('user')"
:filters="[]" :filters="[]"
:filter-method="filterUsernameMethod" :filter-method="filterUsernameMethod"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column <vxe-table-column
field="operate_type" field="operate_type"
:filters="[ :filters="[
{ value: 0, label: $t('new') }, { value: 0, label: $t('new') },
{ value: 1, label: $t('delete') }, { value: 1, label: $t('delete') },
{ value: 3, label: $t('update') }, { value: 3, label: $t('update') },
]" ]"
:filter-method="filterOperateMethod" :filter-method="filterOperateMethod"
:title="$t('operation')" :title="$t('operation')"
> >
<template #default="{ row }"> <template #default="{ row }">
{{ operateTypeMap[row.operate_type] }} {{ operateTypeMap[row.operate_type] }}
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column <vxe-table-column
field="attr_alias" field="attr_alias"
:title="$t('cmdb.attribute')" :title="$t('cmdb.attribute')"
:filters="[]" :filters="[]"
:filter-method="filterAttrMethod" :filter-method="filterAttrMethod"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column> <vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column> <vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column>
</vxe-table> </vxe-table>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_4"> <a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span> <span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" /> <TriggerTable :ci_id="ci._id" />
</div> </div>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<a-empty <a-empty
v-else v-else
:image-style="{ :image-style="{
height: '100px', height: '100px',
}" }"
:style="{ paddingTop: '20%' }" :style="{ paddingTop: '20%' }"
> >
<img slot="image" :src="require('@/assets/data_empty.png')" /> <img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span> <span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty> </a-empty>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui' import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history' import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci' import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue' import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue' import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue' import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default { export default {
name: 'CiDetailTab', name: 'CiDetailTab',
components: { components: {
ElDescriptions: Descriptions, ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem, ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent, CiDetailAttrContent,
CiDetailRelation, CiDetailRelation,
TriggerTable, TriggerTable,
}, },
props: { props: {
typeId: { typeId: {
type: Number, type: Number,
required: true, required: true,
}, },
treeViewsLevels: { treeViewsLevels: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
ci: {}, ci: {},
attributeGroups: [], attributeGroups: [],
activeTabKey: 'tab_1', activeTabKey: 'tab_1',
rowSpanMap: {}, rowSpanMap: {},
ciHistory: [], ciHistory: [],
ciId: null, ciId: null,
ci_types: [], ci_types: [],
hasPermission: true, hasPermission: true,
} }
}, },
computed: { computed: {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
operateTypeMap() { operateTypeMap() {
return { return {
0: this.$t('new'), 0: this.$t('new'),
1: this.$t('delete'), 1: this.$t('delete'),
2: this.$t('update'), 2: this.$t('update'),
} }
}, },
}, },
provide() { provide() {
return { return {
ci_types: () => { ci_types: () => {
return this.ci_types return this.ci_types
}, },
} }
}, },
inject: { inject: {
reload: { reload: {
from: 'reload', from: 'reload',
default: null, default: null,
}, },
handleSearch: { handleSearch: {
from: 'handleSearch', from: 'handleSearch',
default: null, default: null,
}, },
attrList: { attrList: {
from: 'attrList', from: 'attrList',
default: () => [], default: () => [],
}, },
}, },
methods: { methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') { async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') { if (activeTabKey === 'tab_2') {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
}) })
} }
this.ciId = ciId this.ciId = ciId
await this.getCI() await this.getCI()
if (this.hasPermission) { if (this.hasPermission) {
this.getAttributes() this.getAttributes()
this.getCIHistory() this.getCIHistory()
getCITypes().then((res) => { getCITypes().then((res) => {
this.ci_types = res.ci_types this.ci_types = res.ci_types
}) })
} }
}, },
getAttributes() { getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 }) getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => { .then((res) => {
this.attributeGroups = res this.attributeGroups = res
}) })
.catch((e) => {}) .catch((e) => {})
}, },
async getCI() { async getCI() {
await getCIById(this.ciId) await getCIById(this.ciId)
.then((res) => { .then((res) => {
if (res.result.length) { if (res.result.length) {
this.ci = res.result[0] this.ci = res.result[0]
} else { } else {
this.hasPermission = false this.hasPermission = false
} }
}) })
.catch((e) => {}) .catch((e) => {})
}, },
getCIHistory() { getCIHistory() {
getCIHistory(this.ciId) getCIHistory(this.ciId)
.then((res) => { .then((res) => {
this.ciHistory = res this.ciHistory = res
const rowSpanMap = {} const rowSpanMap = {}
let startIndex = 0 let startIndex = 0
let startCount = 1 let startCount = 1
res.forEach((item, index) => { res.forEach((item, index) => {
if (index === 0) { if (index === 0) {
return return
} }
if (res[index].record_id === res[startIndex].record_id) { if (res[index].record_id === res[startIndex].record_id) {
startCount += 1 startCount += 1
rowSpanMap[index] = 0 rowSpanMap[index] = 0
if (index === res.length - 1) { if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount rowSpanMap[startIndex] = startCount
} }
} else { } else {
rowSpanMap[startIndex] = startCount rowSpanMap[startIndex] = startCount
startIndex = index startIndex = index
startCount = 1 startCount = 1
if (index === res.length - 1) { if (index === res.length - 1) {
rowSpanMap[index] = 1 rowSpanMap[index] = 1
} }
} }
}) })
this.rowSpanMap = rowSpanMap this.rowSpanMap = rowSpanMap
}) })
.catch((e) => { .catch((e) => {
console.log(e) console.log(e)
}) })
}, },
changeTab(key) { changeTab(key) {
this.activeTabKey = key this.activeTabKey = key
if (key === 'tab_3') { if (key === 'tab_3') {
this.$nextTick(() => { this.$nextTick(() => {
const $table = this.$refs.xTable const $table = this.$refs.xTable
if ($table) { if ($table) {
const usernameColumn = $table.getColumnByField('username') const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias') const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) { if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))] const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter( $table.setFilter(
usernameColumn, usernameColumn,
usernameList.map((item) => { usernameList.map((item) => {
return { return {
value: item, value: item,
label: item, label: item,
} }
}) })
) )
} }
if (attrColumn) { if (attrColumn) {
$table.setFilter( $table.setFilter(
attrColumn, attrColumn,
this.attrList().map((attr) => { this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name } return { value: attr.alias || attr.name, label: attr.alias || attr.name }
}) })
) )
} }
} }
}) })
} }
}, },
filterUsernameMethod({ value, row, column }) { filterUsernameMethod({ value, row, column }) {
return row.username === value return row.username === value
}, },
filterOperateMethod({ value, row, column }) { filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value) return Number(row.operate_type) === Number(value)
}, },
filterAttrMethod({ value, row, column }) { filterAttrMethod({ value, row, column }) {
return row.attr_alias === value return row.attr_alias === value
}, },
refresh(editAttrName) { refresh(editAttrName) {
this.getCI() this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName) const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload // 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => { setTimeout(() => {
if (_find) { if (_find) {
if (this.reload) { if (this.reload) {
this.reload() this.reload()
} }
} else { } else {
if (this.handleSearch) { if (this.handleSearch) {
this.handleSearch() this.handleSearch()
} }
} }
}, 500) }, 500)
}, },
mergeRowMethod({ row, _rowIndex, column, visibleData }) { mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username'] const fields = ['created_at', 'username']
const cellValue1 = row['created_at'] const cellValue1 = row['created_at']
const cellValue2 = row['username'] const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) { if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1] const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1] let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) { if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 } return { rowspan: 0, colspan: 0 }
} else { } else {
let countRowspan = 1 let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) { while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex] nextRow = visibleData[++countRowspan + _rowIndex]
} }
if (countRowspan > 1) { if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 } return { rowspan: countRowspan, colspan: 1 }
} }
} }
} }
}, },
updateCIByself(params, editAttrName) { updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params } const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName) const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload // 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => { setTimeout(() => {
if (_find) { if (_find) {
if (this.reload) { if (this.reload) {
this.reload() this.reload()
} }
} else { } else {
if (this.handleSearch) { if (this.handleSearch) {
this.handleSearch() this.handleSearch()
} }
} }
}, 500) }, 500)
}, },
shareCi() { shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}` const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text) this.$copyText(text)
.then(() => { .then(() => {
this.$message.success(this.$t('copySuccess')) this.$message.success(this.$t('copySuccess'))
}) })
.catch(() => { .catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed')) this.$message.error(this.$t('cmdb.ci.copyFailed'))
}) })
}, },
}, },
} }
</script> </script>
<style lang="less"> <style lang="less">
.ci-detail-tab { .ci-detail-tab {
height: 100%; height: 100%;
.ant-tabs-content { .ant-tabs-content {
height: calc(100% - 45px); height: calc(100% - 45px);
.ant-tabs-tabpane { .ant-tabs-tabpane {
height: 100%; height: 100%;
} }
} }
.ant-tabs-bar { .ant-tabs-bar {
margin: 0; margin: 0;
} }
.ant-tabs-extra-content { .ant-tabs-extra-content {
line-height: 44px; line-height: 44px;
} }
.ci-detail-attr { .ci-detail-attr {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding: 24px; padding: 24px;
.el-descriptions-item__content { .el-descriptions-item__content {
cursor: default; cursor: default;
&:hover a { &:hover a {
opacity: 1 !important; opacity: 1 !important;
} }
} }
.el-descriptions:first-child > .el-descriptions__header { .el-descriptions:first-child > .el-descriptions__header {
margin-top: 0; margin-top: 0;
} }
.el-descriptions__header { .el-descriptions__header {
margin-bottom: 5px; margin-bottom: 5px;
margin-top: 20px; margin-top: 20px;
} }
.ant-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
.ant-form-item-control { .ant-form-item-control {
line-height: 19px; line-height: 19px;
} }
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,224 +1,250 @@
<template> <template>
<a-modal <a-modal
v-model="visible" v-model="visible"
width="90%" width="90%"
:closable="false" :closable="false"
:centered="true" :centered="true"
:maskClosable="false" :maskClosable="false"
:destroyOnClose="true" :destroyOnClose="true"
@cancel="handleClose" @cancel="handleClose"
@ok="handleOk" @ok="handleOk"
> >
<div :style="{ width: '100%' }" id="add-table-modal"> <div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<!-- <a-input <SearchForm
v-model="expression" ref="searchForm"
class="ci-searchform-expression" :typeId="addTypeId"
:style="{ width, marginBottom: '10px' }" :preferenceAttrList="preferenceAttrList"
:placeholder="placeholder" @refresh="handleSearch"
@focus=" >
() => { <a-button
isFocusExpression = true @click="
} () => {
" $refs.createInstanceForm.handleOpen(true, 'create')
/> --> }
<SearchForm "
ref="searchForm" slot="extraContent"
:typeId="addTypeId" type="primary"
:preferenceAttrList="preferenceAttrList" size="small"
@refresh="handleSearch" >新增</a-button
/> >
<!-- <a @click="handleSearch"><a-icon type="search"/></a> --> </SearchForm>
<vxe-table <vxe-table
ref="xTable" ref="xTable"
row-id="_id" row-id="_id"
:data="tableData" :data="tableData"
:height="tableHeight" :height="tableHeight"
highlight-hover-row highlight-hover-row
:checkbox-config="{ reserve: true }" :checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange" @checkbox-change="onSelectChange"
@checkbox-all="onSelectChange" @checkbox-all="onSelectChange"
show-overflow="tooltip" show-overflow="tooltip"
show-header-overflow="tooltip" show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }" :scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }" :scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table" class="ops-stripe-table"
> >
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column> <vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column <vxe-table-column
v-for="col in columns" v-for="col in columns"
:key="col.field" :key="col.field"
:title="col.title" :title="col.title"
:field="col.field" :field="col.field"
:width="col.width" :width="col.width"
:sortable="col.sortable" :sortable="col.sortable"
> >
<template #default="{row}" v-if="col.value_type === '6'"> <template #default="{row}" v-if="col.value_type === '6'">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span> <span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template> </template>
</vxe-table-column> </vxe-table-column>
</vxe-table> </vxe-table>
<a-pagination <a-pagination
v-model="currentPage" v-model="currentPage"
size="small" size="small"
:total="totalNumber" :total="totalNumber"
show-quick-jumper show-quick-jumper
:page-size="50" :page-size="50"
:show-total=" :show-total="
(total, range) => (total, range) =>
$t('pagination.total', { $t('pagination.total', {
range0: range[0], range0: range[0],
range1: range[1], range1: range[1],
total, total,
}) })
" "
:style="{ textAlign: 'right', marginTop: '10px' }" :style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage" @change="handleChangePage"
/> />
</a-spin> </a-spin>
</div> </div>
</a-modal> <CreateInstanceForm
</template> ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
<script> @reload="
/* eslint-disable no-useless-escape */ () => {
import { searchCI } from '@/modules/cmdb/api/ci' currentPage = 1
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference' getTableData(true)
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation' }
import { getCITableColumns } from '../../../utils/helper' "
import SearchForm from '../../../components/searchForm/SearchForm.vue' />
export default { </a-modal>
name: 'AddTableModal', </template>
components: { SearchForm },
data() { <script>
return { import { searchCI } from '@/modules/cmdb/api/ci'
visible: false, import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
currentPage: 1, import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
totalNumber: 0, import { getCITableColumns } from '../../../utils/helper'
tableData: [], import SearchForm from '../../../components/searchForm/SearchForm.vue'
columns: [], import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
ciObj: {}, import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
ciId: null,
addTypeId: null, export default {
loading: false, name: 'AddTableModal',
expression: '', components: { SearchForm, CreateInstanceForm },
isFocusExpression: false, data() {
type: 'children', return {
preferenceAttrList: [], visible: false,
ancestor_ids: undefined, currentPage: 1,
} totalNumber: 0,
}, tableData: [],
computed: { columns: [],
tableHeight() { ciObj: {},
return this.$store.state.windowHeight - 250 ciId: null,
}, addTypeId: null,
placeholder() { loading: false,
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2') expression: '',
}, isFocusExpression: false,
width() { type: 'children',
return this.isFocusExpression ? '500px' : '100px' preferenceAttrList: [],
}, ancestor_ids: undefined,
}, attrList1: [],
watch: {}, }
methods: { },
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) { computed: {
console.log(ciObj, ciId, addTypeId, type) tableHeight() {
this.visible = true return this.$store.state.windowHeight - 250
this.ciObj = ciObj },
this.ciId = ciId placeholder() {
this.addTypeId = addTypeId return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
this.type = type },
this.ancestor_ids = ancestor_ids width() {
await getSubscribeAttributes(addTypeId).then((res) => { return this.isFocusExpression ? '500px' : '100px'
this.preferenceAttrList = res.attributes // 已经订阅的全部列 },
}) },
this.getTableData(true) provide() {
}, return {
async getTableData(isInit) { attrList: () => {
if (this.addTypeId) { return this.attrList
await this.fetchData(isInit) },
} }
}, },
async fetchData(isInit) { watch: {},
this.loading = true methods: {
// if (isInit) { async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
// const subscribed = await getSubscribeAttributes(this.addTypeId) console.log(ciObj, ciId, addTypeId, type)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列 this.visible = true
// } this.ciObj = ciObj
let sort, fuzzySearch, expression, exp this.ciId = ciId
if (!isInit) { this.addTypeId = addTypeId
fuzzySearch = this.$refs['searchForm'].fuzzySearch this.type = type
expression = this.$refs['searchForm'].expression || '' this.ancestor_ids = ancestor_ids
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
exp = expression.match(regQ) ? expression.match(regQ)[0] : null })
} getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
await searchCI({ })
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`, this.getTableData(true)
count: 50, },
page: this.currentPage, async getTableData(isInit) {
sort, if (this.addTypeId) {
}) await this.fetchData(isInit)
.then((res) => { }
this.tableData = res.result },
this.totalNumber = res.numfound async fetchData(isInit) {
this.columns = this.getColumns(res.result, this.preferenceAttrList) this.loading = true
this.$nextTick(() => { // if (isInit) {
const _table = this.$refs.xTable // const subscribed = await getSubscribeAttributes(this.addTypeId)
if (_table) { // this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
_table.refreshColumn() // }
} let sort, fuzzySearch, expression, exp
this.loading = false if (!isInit) {
}) fuzzySearch = this.$refs['searchForm'].fuzzySearch
}) expression = this.$refs['searchForm'].expression || ''
.catch(() => { const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
this.loading = false
}) exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}, }
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal') await searchCI({
if (modalDom) { q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
const width = modalDom.clientWidth - 50 count: 50,
return getCITableColumns(data, attrList, width) page: this.currentPage,
} sort,
return [] })
}, .then((res) => {
onSelectChange() {}, this.tableData = res.result
handleClose() { this.totalNumber = res.numfound
this.$refs.xTable.clearCheckboxRow() this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.currentPage = 1 this.$nextTick(() => {
this.expression = '' const _table = this.$refs.xTable
this.isFocusExpression = false if (_table) {
this.visible = false _table.refreshColumn()
}, }
async handleOk() { this.loading = false
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords() })
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords() })
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id) .catch(() => {
if (ciIds.length) { this.loading = false
if (this.type === 'children') { })
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids) },
} else { getColumns(data, attrList) {
await batchUpdateCIRelationParents(ciIds, [this.ciId]) const modalDom = document.getElementById('add-table-modal')
} if (modalDom) {
setTimeout(() => { const width = modalDom.clientWidth - 50
this.$message.success(this.$t('addSuccess')) return getCITableColumns(data, attrList, width)
this.handleClose() }
this.$emit('reload') return []
}, 500) },
} onSelectChange() {},
}, handleClose() {
handleSearch() { this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1 this.currentPage = 1
this.fetchData() this.expression = ''
}, this.isFocusExpression = false
handleChangePage(page, pageSize) { this.visible = false
this.currentPage = page },
this.fetchData() async handleOk() {
}, const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
}, const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
} const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
</script> if (ciIds.length) {
if (this.type === 'children') {
<style lang="less" scoped></style> await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>

View File

@ -1,166 +1,253 @@
<template> <template>
<a-dropdown :trigger="['contextmenu']"> <div
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)"> :class="{
<a-menu-item v-for="item in menuList" :key="item.id">{{ $t('new') }} {{ item.alias }}</a-menu-item> 'relation-views-node': true,
<a-menu-item v-if="showDelete" key="delete">{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item> 'relation-views-node-checkbox': showCheckbox,
</a-menu> }"
<div @click="clickNode"
:style="{ >
width: '100%', <span>
display: 'inline-flex', <a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
justifyContent: 'space-between', <template v-if="icon">
alignItems: 'center', <img
}" v-if="icon.includes('$$') && icon.split('$$')[2]"
@click="clickNode" :src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
> :style="{ maxHeight: '14px', maxWidth: '14px' }"
<span />
:style="{ <ops-icon
display: 'flex', v-else-if="icon.includes('$$') && icon.split('$$')[0]"
overflow: 'hidden', :style="{
width: '100%', color: icon.split('$$')[1],
textOverflow: 'ellipsis', fontSize: '14px',
whiteSpace: 'nowrap', }"
alignItems: 'center', :type="icon.split('$$')[0]"
}" />
> <span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
<template v-if="icon"> </template>
<img <span class="relation-views-node-title">{{ this.title }}</span>
v-if="icon.split('$$')[2]" </span>
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`" <a-dropdown>
:style="{ maxHeight: '14px', maxWidth: '14px' }" <a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
/> <template v-if="showBatchLevel === null">
<ops-icon <a-menu-item
v-else v-for="item in menuList"
:style="{ :key="item.id"
color: icon.split('$$')[1], ><a-icon type="plus-circle" />{{ $t('new') }} {{ item.alias }}</a-menu-item
fontSize: '14px', >
}" <a-menu-item
:type="icon.split('$$')[0]" v-if="showDelete"
/> key="delete"
</template> ><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item
<span >
:style="{ <a-menu-divider />
display: 'inline-block', <a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
width: '16px', <a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
height: '16px', <a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
borderRadius: '50%', <a-menu-divider />
backgroundColor: '#d3d3d3', <a-menu-item
color: '#fff', key="batch"
textAlign: 'center', ><ops-icon type="icon-xianxing-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item
lineHeight: '16px', >
fontSize: '12px', </template>
}" <template v-else>
v-else <a-menu-item
>{{ ciTypeName ? ciTypeName[0].toUpperCase() : 'i' }}</span :disabled="!batchTreeKey || !batchTreeKey.length"
> key="batchGrant"
<span :style="{ marginLeft: '5px' }">{{ this.title }}</span> ><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
</span> >
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon> <a-menu-item
</div> :disabled="!batchTreeKey || !batchTreeKey.length"
</a-dropdown> key="batchRevoke"
</template> ><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<script> <a-menu-divider />
export default { <template v-if="showBatchLevel > 0">
name: 'ContextMenu', <a-menu-item
props: { :disabled="!batchTreeKey || !batchTreeKey.length"
title: { key="batchDelete"
type: String, ><ops-icon type="icon-xianxing-delete" />{{ $t('delete') }}</a-menu-item
default: '', >
}, <a-menu-divider />
treeKey: { </template>
type: String, <a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
default: '', </template>
}, </a-menu>
levels: { <a-icon class="relation-views-node-operation" type="ellipsis" />
type: Array, </a-dropdown>
default: () => [], <a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
}, </div>
currentViews: { </template>
type: Object,
default: () => {}, <script>
}, export default {
id2type: { name: 'ContextMenu',
type: Object, props: {
default: () => {}, title: {
}, type: String,
isLeaf: { default: '',
type: Boolean, },
default: () => false, treeKey: {
}, type: String,
ciTypes: { default: '',
type: Array, },
default: () => [], levels: {
}, type: Array,
}, default: () => [],
data() { },
return { currentViews: {
switchIcon: 'down', type: Object,
} default: () => {},
}, },
computed: { id2type: {
childLength() { type: Object,
const reg = /(?<=\()\S+(?=\))/g default: () => {},
return Number(this.title.match(reg)[0]) },
}, isLeaf: {
splitTreeKey() { type: Boolean,
return this.treeKey.split('@^@') default: () => false,
}, },
_tempTree() { ciTypeIcons: {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%') type: Object,
}, default: () => {},
_typeIdIdx() { },
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index showBatchLevel: {
}, type: Number,
showDelete() { default: null,
if (this._typeIdIdx === 0) { },
// 如果是第一层节点则不能删除 batchTreeKey: {
return false type: Array,
} default: () => [],
return true },
}, },
menuList() { data() {
let _menuList = [] return {
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) { switchIcon: 'down',
// 不是叶子节点 }
const id = Number(this.levels[this._typeIdIdx + 1]) },
_menuList = [ computed: {
{ childLength() {
id, const reg = /(?<=\()\S+(?=\))/g
alias: this.id2type[id].alias || this.id2type[id].name, return Number(this.title.match(reg)[0])
}, },
] splitTreeKey() {
} else { return this.treeKey.split('@^@')
// 叶子节点 },
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => { _tempTree() {
return { id: item.id, alias: item.alias || item.name } return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
}) },
} _typeIdIdx() {
return _menuList return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
}, },
icon() { showDelete() {
const _split = this.treeKey.split('@^@') if (this._typeIdIdx === 0) {
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] // 如果是第一层节点则不能删除
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) return false
return _find?.icon || null }
}, return true
ciTypeName() { },
const _split = this.treeKey.split('@^@') menuList() {
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] let _menuList = []
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
return _find?.name || '' // 不是叶子节点
}, const id = Number(this.levels[this._typeIdIdx + 1])
}, _menuList = [
methods: { {
onContextMenuClick(treeKey, menuKey) { id,
this.$emit('onContextMenuClick', treeKey, menuKey) alias: this.id2type[id].alias || this.id2type[id].name,
}, },
clickNode() { ]
this.$emit('onNodeClick', this.treeKey) } else {
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down' // 叶子节点
}, _menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
}, return { id: item.id, alias: item.alias || item.name }
} })
</script> }
return _menuList
<style></style> },
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
},
}
</script>
<style lang="less" scoped>
.relation-views-node {
width: 100%;
display: inline-flex;
justify-content: space-between;
align-items: center;
> span {
display: flex;
overflow: hidden;
align-items: center;
width: 100%;
.relation-views-node-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #d3d3d3;
color: #fff;
text-align: center;
line-height: 16px;
font-size: 12px;
}
.relation-views-node-title {
padding-left: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: calc(100% - 16px);
}
}
.relation-views-node-operation {
display: none;
margin-right: 5px;
}
}
.relation-views-node-checkbox,
.relation-views-node-moveright {
> span {
.relation-views-node-checkbox {
margin-right: 10px;
}
.relation-views-node-title {
width: calc(100% - 42px);
}
}
}
</style>
<style lang="less">
.relation-views-left .ant-tree-node-content-wrapper:hover {
.relation-views-node-operation {
display: inline-block;
}
}
.relation-views-left {
ul:has(.relation-views-node-checkbox) > li > ul {
margin-left: 26px;
}
ul:has(.relation-views-node-checkbox) {
margin-left: 0 !important;
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<a-modal
width="600px"
:bodyStyle="{
paddingTop: 0,
}"
:visible="visible"
:footer="null"
@cancel="handleCancel"
:title="$t('view')"
>
<div>
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
<p>
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
<a
@click="
() => {
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
}
"
v-if="readCIIdFilterPermissions.length > 10"
><a-icon
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
/></a>
</p>
<a-tag
v-for="item in showAllReadCIIdFilterPermissions
? readCIIdFilterPermissions
: readCIIdFilterPermissions.slice(0, 10)"
:key="item.name"
color="blue"
:style="{ marginBottom: '5px' }"
>{{ item.name }}</a-tag
>
<a-tag
:style="{ marginBottom: '5px' }"
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
>
</template>
<a-empty v-else>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</div>
</a-modal>
</template>
<script>
import { ciTypeFilterPermissions, getCIType } from '../../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'ReadPermissionsModal',
components: { FilterComp },
data() {
return {
visible: false,
filerPerimissions: {},
readCIIdFilterPermissions: [],
canSearchPreferenceAttrList: [],
showAllReadCIIdFilterPermissions: false,
allRoles: [],
}
},
mounted() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
},
async open(treeKey) {
this.visible = true
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
const typeId = _treeKey[1]
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
await ciTypeFilterPermissions(typeId).then((res) => {
this.filerPerimissions = res
})
const readCIIdFilterPermissions = []
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
const { id_filter } = v
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
const _find = this.allRoles.find((item) => item.id === Number(k))
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
}
})
this.readCIIdFilterPermissions = readCIIdFilterPermissions
console.log(readCIIdFilterPermissions)
},
handleCancel() {
this.showAllReadCIIdFilterPermissions = false
this.visible = false
},
},
}
</script>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@ -1,97 +1,106 @@
<template> <template>
<treeselect <treeselect
:disable-branch-nodes="multiple ? false : true" :disable-branch-nodes="multiple ? false : true"
:multiple="multiple" :multiple="multiple"
:options="employeeTreeSelectOption" :options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')" :placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue" v-model="treeValue"
:max-height="200" :max-height="200"
:noChildrenText="$t('cs.components.empty')" :noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')" :noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'" :class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:limit="20" :limit="limit"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
v-bind="$attrs" v-bind="$attrs"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
> :flat="flat"
</treeselect> >
</template> </treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect' <script>
import { formatOption } from '@/utils/util' import Treeselect from '@riophae/vue-treeselect'
export default { import { formatOption } from '@/utils/util'
name: 'EmployeeTreeSelect', export default {
components: { name: 'EmployeeTreeSelect',
Treeselect, components: {
}, Treeselect,
model: { },
prop: 'value', model: {
event: 'change', prop: 'value',
}, event: 'change',
props: { },
value: { props: {
type: [String, Array, Number, null], value: {
default: null, type: [String, Array, Number, null],
}, default: null,
multiple: { },
type: Boolean, multiple: {
default: false, type: Boolean,
}, default: false,
className: { },
type: String, className: {
default: 'ops-setting-treeselect', type: String,
}, default: 'ops-setting-treeselect',
placeholder: { },
type: String, placeholder: {
default: '', type: String,
}, default: '',
idType: { },
type: Number, idType: {
default: 1, type: Number,
}, default: 1,
departmentKey: { },
type: String, departmentKey: {
default: 'department_id', type: String,
}, default: 'department_id',
employeeKey: { },
type: String, employeeKey: {
default: 'employee_id', type: String,
}, default: 'employee_id',
}, },
data() { limit: {
return {} type: Number,
}, default: 20,
inject: { },
provide_allTreeDepAndEmp: { flat: {
from: 'provide_allTreeDepAndEmp', type: Boolean,
}, default: false,
readOnly: { },
from: 'readOnly', },
default: false, data() {
}, return {}
}, },
computed: { inject: {
treeValue: { provide_allTreeDepAndEmp: {
get() { from: 'provide_allTreeDepAndEmp',
return this.value },
}, readOnly: {
set(val) { from: 'readOnly',
this.$emit('change', val) default: false,
return val },
}, },
}, computed: {
allTreeDepAndEmp() { treeValue: {
return this.provide_allTreeDepAndEmp() get() {
}, return this.value
employeeTreeSelectOption() { },
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey) set(val) {
}, this.$emit('change', val)
}, return val
methods: {}, },
} },
</script> allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
<style scoped></style> },
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
},
},
methods: {},
}
</script>
<style scoped></style>