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'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ 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: '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: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ 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: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ 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: '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: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ 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: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@ -1,332 +1,346 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} 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>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// 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
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// 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
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,492 +1,503 @@
const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
menu: {
views: 'Views',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
ciTree: 'Tree Views',
ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool',
preference: 'Preference',
batchUpload: 'Batch Import',
citypeManage: 'Modeling',
backendManage: 'Backend',
customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail'
},
ciType: {
ciType: 'CIType',
attributes: 'Attributes',
relation: 'Relation',
trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery',
grant: 'Grant',
addGroup: 'New Group',
editGroup: 'Edit Group',
group: 'Group',
attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType',
editGroupName: 'Edit group name',
deleteGroup: 'Delete this group',
CITypeName: 'Name(English)',
English: 'English',
inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType',
defaultSort: 'Default sort',
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies',
uniqueKeySelect: 'Please select a unique identifier',
notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploading: 'Uploading',
uploadFailed: 'Upload failed, please try again later',
addPlugin: 'New plugin',
deletePlugin: 'Delete plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
attributeMap: 'Attribute mapping',
autoDiscovery: 'AutoDiscovery',
node: 'Node',
adExecConfig: 'Execute configuration',
adExecTarget: 'Execute targets',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
selectFromCMDBTips: 'Select from CMDB ',
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
username: 'Username',
password: 'Password',
link: 'Link',
list: 'List',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
computeForAllCITips: 'All CI trigger computes',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
isUnique: 'Is it unique',
unique: 'Unique',
isChoice: 'Choiced',
defaultShow: 'Default Display',
defaultShowTips: 'The CI instance table displays this field by default',
isSortable: 'Sortable',
isIndex: 'Indexed',
index: 'Index',
indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete: 'Confirm to delete [{name}]?',
confirmDelete2: 'Confirm to delete?',
computeSuccess: 'Triggered successfully!',
basicConfig: 'Basic Settings',
AttributeName: 'Name(English)',
DataType: 'Data Type',
defaultValue: 'Default value',
autoIncID: 'Auto-increment ID',
customTime: 'Custom time',
advancedSettings: 'Advanced Settings',
font: 'Font',
color: 'Color',
choiceValue: 'Predefined value',
computedAttribute: 'Computed 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 }}',
addAttribute: 'New attribute',
existedAttributes: 'Already have attributes',
editAttribute: 'Edit attribute',
addAttributeTips1: 'If sorting is selected, it must also be selected!',
uniqueConstraint: 'Unique Constraint',
up: 'Move up',
down: 'Move down',
selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
buildinAttribute: 'built-in attributes',
expr: 'Expression',
code: 'Code',
apply: 'apply',
continueAdd: 'Keep adding',
filter: 'Filter',
choiceOther: 'Other CIType Attributes',
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 ]',
selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select 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 []',
valueExisted: 'The current value already exists!',
addRelation: 'Add Relation',
sourceCIType: 'Source CIType',
sourceCITypeTips: 'Please select Source CIType',
dstCIType: 'Target CIType',
dstCITypeTips: 'Please select target CIType',
relationType: 'Relation Type',
relationTypeTips: 'Please select relation type',
isParent: 'is parent',
relationConstraint: 'Constraints',
relationConstraintTips: 'please select a relationship constraint',
one2Many: 'One to Many',
one2One: 'One to One',
many2Many: 'Many to Many',
basicInfo: 'Basic Information',
nameInputTips: 'Please enter name',
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
changeInstance: 'Instance changes',
selectMutipleAttributes: 'Please select attributes (multiple selections)',
selectSingleAttribute: 'Please select an attribute (single choice)',
beforeDays: 'ahead of time',
days: 'Days',
notifyAt: 'Send time',
notify: 'Notify',
triggerAction: 'Trigger action',
receivers: 'Recipients',
emailTips: 'Please enter your email address, separate multiple email addresses with ;',
customEmail: 'Custom recipients',
notifySubject: 'Notification title',
notifySubjectTips: 'Please enter notification title',
notifyContent: 'Content',
notifyMethod: 'Notify methods',
botSelect: 'Please select a robot',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger',
editTriggerTitle: 'Edit trigger {name}',
newTriggerTitle: 'Add trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer',
float: 'Float',
text: 'Text',
datetime: 'DateTime',
date: 'Date',
time: 'Time',
json: 'JSON',
event: 'Event',
reg: 'Regex',
isInherit: 'Inherit',
inheritType: 'Inherit Type',
inheritTypePlaceholder: 'Please select inherit types',
inheritFrom: 'inherit from {name}',
groupInheritFrom: 'Please go to the {name} for modification'
},
components: {
unselectAttributes: 'Unselected',
selectAttributes: 'Selected',
downloadCI: 'Export data',
filename: 'Filename',
filenameInputTips: 'Please enter filename',
saveType: 'Save type',
saveTypeTips: 'Please select save type',
xlsx: 'Excel workbook (*.xlsx)',
csv: 'CSV (comma separated) (*.csv)',
html: 'Web page (*.html)',
xml: 'XML data (*.xml)',
txt: 'Text file (tab delimited) (*.txt)',
grantUser: 'Grant User/Department',
grantRole: 'Grant Role',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readAttribute: 'View Attributes',
readCI: 'View CIs',
config: 'Configuration',
ciTypeGrant: 'Grant CIType',
ciGrant: 'Grant CI',
attributeGrant: 'Grant Attribute',
relationGrant: 'Grant Relation',
perm: 'Permissions',
all: 'All',
customize: 'Customize',
none: 'None',
customizeFilterName: 'Please enter a custom filter name',
colorPickerError: 'Initialization color format error, use #fff or rgb format',
example: 'Example value',
aliyun: 'aliyun',
tencentcloud: 'Tencent Cloud',
huaweicloud: 'Huawei Cloud',
beforeChange: 'Before change',
afterChange: 'After change',
noticeContentTips: 'Please enter notification content',
saveQuery: 'Save Filters',
pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description',
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',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType',
already: 'already',
not: 'not',
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
requestParam: 'Request parameters',
param: 'Parameter{param}',
value: 'Value{value}',
clear: 'Clear',
},
batch: {
downloadFailed: 'Download failed',
unselectCIType: 'No CIType selected yet',
pleaseUploadFile: 'Please upload files',
batchUploadCanceled: 'Batch upload canceled',
selectCITypeTips: 'Please select CIType',
downloadTemplate: 'Download Template',
drawTips: 'Click or drag files here to upload!',
supportFileTypes: 'Supported file types: xls, xlsx',
uploadResult: 'Upload results',
total: 'total',
successItems: 'items, succeeded',
failedItems: 'items, failed',
items: 'items',
errorTips: 'Error message',
requestFailedTips: 'An error occurred with the request, please try again later',
requestSuccessTips: 'Upload completed',
},
preference: {
mySub: 'My Subscription',
sub: 'Subscribe',
cancelSub: 'Unsubscribe',
editSub: 'Edit subscription',
peopleSub: ' people subscribed',
noSub: 'No subscribed',
cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub: 'Are you sure to cancel your subscription?',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
of: 'of',
hoursAgo: 'hours ago',
daysAgo: 'days ago',
monthsAgo: 'month ago',
yearsAgo: 'years ago',
just: 'just now',
},
custom_dashboard: {
charts: 'Chart',
newChart: 'Add Chart',
editChart: 'Edit Chart',
title: 'Title',
titleTips: 'Please enter a chart title',
calcIndicators: 'Counter',
dimensions: 'Dimensions',
selectDimensions: 'Please select a dimension',
quantity: 'Quantity',
childCIType: 'Relational CIType',
level: 'Level',
levelTips: 'Please enter the relationship level',
preview: 'Preview',
showIcon: 'Display icon',
chartType: 'Chart Type',
dataFilter: 'Data Filtering',
format: 'Formats',
fontColor: 'Font Color',
backgroundColor: 'Background',
chartColor: 'Chart Color',
chartLength: 'Length',
barType: 'Bar Type',
stackedBar: 'Stacked Bar',
multipleSeriesBar: 'Multiple Series Bar ',
axis: 'Axis',
direction: 'Direction',
lowerShadow: 'Lower Shadow',
count: 'Indicator',
bar: 'Bar',
line: 'Line',
pie: 'Pie',
table: 'Table',
default: 'default',
relation: 'Relation',
noCustomDashboard: 'The administrator has not customized the dashboard yet',
},
preference_relation: {
newServiceTree: 'Add ServiceTree',
serviceTreeName: 'Name',
public: 'Public',
saveLayout: 'Save Layout',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips1: 'Cannot form a view with the currently selected node, please select again!',
tips2: 'Please enter the new serviceTree name!',
tips3: 'Please select at least two nodes!',
},
history: {
ciChange: 'CI',
relationChange: 'Relation',
ciTypeChange: 'CIType',
triggerHistory: 'Triggers',
opreateTime: 'Operate Time',
user: 'User',
userTips: 'Enter filter username',
filter: 'Search',
filterOperate: 'fitler operation',
attribute: 'Attribute',
old: 'Old',
new: 'New',
noUpdate: 'No update',
itemsPerPage: '/page',
triggerName: 'Name',
event: 'Event',
action: 'Actoin',
status: 'Status',
done: 'Done',
undone: 'Undone',
triggerTime: 'Trigger Time',
totalItems: '{total} records in total',
pleaseSelect: 'Please select',
startTime: 'Start Time',
endTime: 'End Time',
deleteCIType: 'Delete CIType',
addCIType: 'Add CIType',
updateCIType: 'Update CIType',
addAttribute: 'Add Attribute',
updateAttribute: 'Update Attribute',
deleteAttribute: 'Delete Attribute',
addTrigger: 'Add Trigger',
updateTrigger: 'Update Trigger',
deleteTrigger: 'Delete Trigger',
addUniqueConstraint: 'Add Unique Constraint',
updateUniqueConstraint: 'Update Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint',
addRelation: 'Add Relation',
deleteRelation: 'Delete Relation',
noModifications: 'No Modifications',
attr: 'attribute',
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',
nameTips: 'Please enter a type name',
},
ad: {
upload: 'Import',
download: 'Export',
accept: 'Accept',
acceptBy: 'Accept By',
acceptTime: 'Accept Time',
confirmAccept: 'Confirm Accept?',
acceptSuccess: 'Accept successfully',
isAccept: 'Is accept',
deleteADC: 'Confirm to delete this data?',
batchDelete: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins',
snmp: 'Network Devices',
http: 'Public Clouds',
rule: 'AutoDiscovery Rules',
timeout: 'Timeout error',
mode: 'Mode',
collectSettings: 'Collection Settings',
updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: Returns the name of a unique attribute
"""
return
@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.
type: String Integer Float Date DateTime Time JSON
For example:
return [
("ci_type", "String", "CIType name"),
("private_ip", "String", "Internal IP, multiple values separated by commas")
]
"""
return []
@staticmethod
def run():
"""
Execution entry, returns collected attribute values
:return:
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
For example:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: The collection return must be a list")
`,
server: 'Server',
vserver: 'VServer',
nic: 'NIC',
disk: 'harddisk',
},
ci: {
attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items',
addRelation: 'Add Relation',
all: 'All',
batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress: 'Currently being updated in batches',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting: 'Deleting...',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
copyFailed: 'Copy failed',
noLevel: 'No hierarchical relationship!',
batchAddRelation: 'Batch Add Relation',
history: 'History',
topo: 'Topology',
table: 'Table',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
confirmDeleteRelation: 'Confirm to delete the relationship?',
tips1: 'Use commas to separate multiple values',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected',
tips5: 'Search name | alias',
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',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
newUpdateField: 'Add a Attribute',
attributeSettings: 'Attribute Settings',
share: 'Share',
noPermission: 'No Permission'
},
serviceTree: {
deleteNode: 'Delete Node',
tips1: 'For example: q=os_version:centos&sort=os_version',
tips2: 'Expression search',
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
}
}
export default cmdb_en
const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
menu: {
views: 'Views',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
ciTree: 'Tree Views',
ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool',
preference: 'Preference',
batchUpload: 'Batch Import',
citypeManage: 'Modeling',
backendManage: 'Backend',
customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail'
},
ciType: {
ciType: 'CIType',
attributes: 'Attributes',
relation: 'Relation',
trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery',
grant: 'Grant',
addGroup: 'New Group',
editGroup: 'Edit Group',
group: 'Group',
attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType',
editGroupName: 'Edit group name',
deleteGroup: 'Delete this group',
CITypeName: 'Name(English)',
English: 'English',
inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType',
defaultSort: 'Default sort',
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploading: 'Uploading',
uploadFailed: 'Upload failed, please try again later',
addPlugin: 'New plugin',
deletePlugin: 'Delete plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
attributeMap: 'Attribute mapping',
autoDiscovery: 'AutoDiscovery',
node: 'Node',
adExecConfig: 'Execute configuration',
adExecTarget: 'Execute targets',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
selectFromCMDBTips: 'Select from CMDB ',
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
username: 'Username',
password: 'Password',
link: 'Link',
list: 'List',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
computeForAllCITips: 'All CI trigger computes',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
isUnique: 'Is it unique',
unique: 'Unique',
isChoice: 'Choiced',
defaultShow: 'Default Display',
defaultShowTips: 'The CI instance table displays this field by default',
isSortable: 'Sortable',
isIndex: 'Indexed',
index: 'Index',
indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete: 'Confirm to delete [{name}]?',
confirmDelete2: 'Confirm to delete?',
computeSuccess: 'Triggered successfully!',
basicConfig: 'Basic Settings',
AttributeName: 'Name(English)',
DataType: 'Data Type',
defaultValue: 'Default value',
autoIncID: 'Auto-increment ID',
customTime: 'Custom time',
advancedSettings: 'Advanced Settings',
font: 'Font',
color: 'Color',
choiceValue: 'Predefined value',
computedAttribute: 'Computed 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 }}',
addAttribute: 'New attribute',
existedAttributes: 'Already have attributes',
editAttribute: 'Edit attribute',
addAttributeTips1: 'If sorting is selected, it must also be selected!',
uniqueConstraint: 'Unique Constraint',
up: 'Move up',
down: 'Move down',
selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
buildinAttribute: 'built-in attributes',
expr: 'Expression',
code: 'Code',
apply: 'apply',
continueAdd: 'Keep adding',
filter: 'Filter',
choiceOther: 'Other CIType Attributes',
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 ]',
selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select 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 []',
valueExisted: 'The current value already exists!',
addRelation: 'Add Relation',
sourceCIType: 'Source CIType',
sourceCITypeTips: 'Please select Source CIType',
dstCIType: 'Target CIType',
dstCITypeTips: 'Please select target CIType',
relationType: 'Relation Type',
relationTypeTips: 'Please select relation type',
isParent: 'is parent',
relationConstraint: 'Constraints',
relationConstraintTips: 'please select a relationship constraint',
one2Many: 'One to Many',
one2One: 'One to One',
many2Many: 'Many to Many',
basicInfo: 'Basic Information',
nameInputTips: 'Please enter name',
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
changeInstance: 'Instance changes',
selectMutipleAttributes: 'Please select attributes (multiple selections)',
selectSingleAttribute: 'Please select an attribute (single choice)',
beforeDays: 'ahead of time',
days: 'Days',
notifyAt: 'Send time',
notify: 'Notify',
triggerAction: 'Trigger action',
receivers: 'Recipients',
emailTips: 'Please enter your email address, separate multiple email addresses with ;',
customEmail: 'Custom recipients',
notifySubject: 'Notification title',
notifySubjectTips: 'Please enter notification title',
notifyContent: 'Content',
notifyMethod: 'Notify methods',
botSelect: 'Please select a robot',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger',
editTriggerTitle: 'Edit trigger {name}',
newTriggerTitle: 'Add trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer',
float: 'Float',
text: 'Text',
datetime: 'DateTime',
date: 'Date',
time: 'Time',
json: 'JSON',
event: 'Event',
reg: 'Regex',
isInherit: 'Inherit',
inheritType: 'Inherit Type',
inheritTypePlaceholder: 'Please select inherit types',
inheritFrom: 'inherit from {name}',
groupInheritFrom: 'Please go to the {name} for modification'
},
components: {
unselectAttributes: 'Unselected',
selectAttributes: 'Selected',
downloadCI: 'Export data',
filename: 'Filename',
filenameInputTips: 'Please enter filename',
saveType: 'Save type',
saveTypeTips: 'Please select save type',
xlsx: 'Excel workbook (*.xlsx)',
csv: 'CSV (comma separated) (*.csv)',
html: 'Web page (*.html)',
xml: 'XML data (*.xml)',
txt: 'Text file (tab delimited) (*.txt)',
grantUser: 'Grant User/Department',
grantRole: 'Grant Role',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readAttribute: 'View Attributes',
readCI: 'View CIs',
config: 'Configuration',
ciTypeGrant: 'Grant CIType',
ciGrant: 'Grant CI',
attributeGrant: 'Grant Attribute',
relationGrant: 'Grant Relation',
perm: 'Permissions',
all: 'All',
customize: 'Customize',
none: 'None',
customizeFilterName: 'Please enter a custom filter name',
colorPickerError: 'Initialization color format error, use #fff or rgb format',
example: 'Example value',
aliyun: 'aliyun',
tencentcloud: 'Tencent Cloud',
huaweicloud: 'Huawei Cloud',
beforeChange: 'Before change',
afterChange: 'After change',
noticeContentTips: 'Please enter notification content',
saveQuery: 'Save Filters',
pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description',
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',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType',
already: 'already',
not: 'not',
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
requestParam: 'Request parameters',
param: 'Parameter{param}',
value: 'Value{value}',
clear: 'Clear',
},
batch: {
downloadFailed: 'Download failed',
unselectCIType: 'No CIType selected yet',
pleaseUploadFile: 'Please upload files',
batchUploadCanceled: 'Batch upload canceled',
selectCITypeTips: 'Please select CIType',
downloadTemplate: 'Download Template',
drawTips: 'Click or drag files here to upload!',
supportFileTypes: 'Supported file types: xls, xlsx',
uploadResult: 'Upload results',
total: 'total',
successItems: 'items, succeeded',
failedItems: 'items, failed',
items: 'items',
errorTips: 'Error message',
requestFailedTips: 'An error occurred with the request, please try again later',
requestSuccessTips: 'Upload completed',
},
preference: {
mySub: 'My Subscription',
sub: 'Subscribe',
cancelSub: 'Unsubscribe',
editSub: 'Edit subscription',
peopleSub: ' people subscribed',
noSub: 'No subscribed',
cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub: 'Are you sure to cancel your subscription?',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
of: 'of',
hoursAgo: 'hours ago',
daysAgo: 'days ago',
monthsAgo: 'month ago',
yearsAgo: 'years ago',
just: 'just now',
},
custom_dashboard: {
charts: 'Chart',
newChart: 'Add Chart',
editChart: 'Edit Chart',
title: 'Title',
titleTips: 'Please enter a chart title',
calcIndicators: 'Counter',
dimensions: 'Dimensions',
selectDimensions: 'Please select a dimension',
quantity: 'Quantity',
childCIType: 'Relational CIType',
level: 'Level',
levelTips: 'Please enter the relationship level',
preview: 'Preview',
showIcon: 'Display icon',
chartType: 'Chart Type',
dataFilter: 'Data Filtering',
format: 'Formats',
fontColor: 'Font Color',
backgroundColor: 'Background',
chartColor: 'Chart Color',
chartLength: 'Length',
barType: 'Bar Type',
stackedBar: 'Stacked Bar',
multipleSeriesBar: 'Multiple Series Bar ',
axis: 'Axis',
direction: 'Direction',
lowerShadow: 'Lower Shadow',
count: 'Indicator',
bar: 'Bar',
line: 'Line',
pie: 'Pie',
table: 'Table',
default: 'default',
relation: 'Relation',
noCustomDashboard: 'The administrator has not customized the dashboard yet',
},
preference_relation: {
newServiceTree: 'Add ServiceTree',
serviceTreeName: 'Name',
public: 'Public',
saveLayout: 'Save Layout',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips1: 'Cannot form a view with the currently selected node, please select again!',
tips2: 'Please enter the new serviceTree name!',
tips3: 'Please select at least two nodes!',
},
history: {
ciChange: 'CI',
relationChange: 'Relation',
ciTypeChange: 'CIType',
triggerHistory: 'Triggers',
opreateTime: 'Operate Time',
user: 'User',
userTips: 'Enter filter username',
filter: 'Search',
filterOperate: 'fitler operation',
attribute: 'Attribute',
old: 'Old',
new: 'New',
noUpdate: 'No update',
itemsPerPage: '/page',
triggerName: 'Name',
event: 'Event',
action: 'Actoin',
status: 'Status',
done: 'Done',
undone: 'Undone',
triggerTime: 'Trigger Time',
totalItems: '{total} records in total',
pleaseSelect: 'Please select',
startTime: 'Start Time',
endTime: 'End Time',
deleteCIType: 'Delete CIType',
addCIType: 'Add CIType',
updateCIType: 'Update CIType',
addAttribute: 'Add Attribute',
updateAttribute: 'Update Attribute',
deleteAttribute: 'Delete Attribute',
addTrigger: 'Add Trigger',
updateTrigger: 'Update Trigger',
deleteTrigger: 'Delete Trigger',
addUniqueConstraint: 'Add Unique Constraint',
updateUniqueConstraint: 'Update Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint',
addRelation: 'Add Relation',
deleteRelation: 'Delete Relation',
noModifications: 'No Modifications',
attr: 'attribute',
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',
nameTips: 'Please enter a type name',
},
ad: {
upload: 'Import',
download: 'Export',
accept: 'Accept',
acceptBy: 'Accept By',
acceptTime: 'Accept Time',
confirmAccept: 'Confirm Accept?',
acceptSuccess: 'Accept successfully',
isAccept: 'Is accept',
deleteADC: 'Confirm to delete this data?',
batchDelete: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins',
snmp: 'Network Devices',
http: 'Public Clouds',
rule: 'AutoDiscovery Rules',
timeout: 'Timeout error',
mode: 'Mode',
collectSettings: 'Collection Settings',
updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: Returns the name of a unique attribute
"""
return
@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.
type: String Integer Float Date DateTime Time JSON
For example:
return [
("ci_type", "String", "CIType name"),
("private_ip", "String", "Internal IP, multiple values separated by commas")
]
"""
return []
@staticmethod
def run():
"""
Execution entry, returns collected attribute values
:return:
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
For example:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: The collection return must be a list")
`,
server: 'Server',
vserver: 'VServer',
nic: 'NIC',
disk: 'harddisk',
},
ci: {
attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items',
addRelation: 'Add Relation',
all: 'All',
batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress: 'Currently being updated in batches',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting: 'Deleting...',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
copyFailed: 'Copy failed',
noLevel: 'No hierarchical relationship!',
batchAddRelation: 'Batch Add Relation',
history: 'History',
topo: 'Topology',
table: 'Table',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
confirmDeleteRelation: 'Confirm to delete the relationship?',
tips1: 'Use commas to separate multiple values',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected',
tips5: 'Search name | alias',
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',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
newUpdateField: 'Add a Attribute',
attributeSettings: 'Attribute Settings',
share: 'Share',
noPermission: 'No Permission'
},
serviceTree: {
deleteNode: 'Delete Node',
tips1: 'For example: q=os_version:centos&sort=os_version',
tips2: 'Expression search',
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
batch: 'Batch',
grantTitle: 'Grant(read)',
userPlaceholder: 'Please select users',
rolePlaceholder: 'Please select roles',
grantedByServiceTree: 'Granted By Service Tree:',
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 = {
relation: '关系',
attribute: '属性',
menu: {
views: '视图',
config: '配置',
backend: '管理端',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
adCIs: '自动发现池',
preference: '我的订阅',
batchUpload: '批量导入',
citypeManage: '模型配置',
backendManage: '后台管理',
customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系',
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情'
},
ciType: {
ciType: '模型',
attributes: '模型属性',
relation: '模型关联',
trigger: '触发器',
attributeAD: '属性自动发现',
relationAD: '关系自动发现',
grant: '权限配置',
addGroup: '新增分组',
editGroup: '修改分组',
group: '分组',
attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型',
editGroupName: '编辑组名称',
deleteGroup: '删除该组',
CITypeName: '模型名(英文)',
English: '英文',
inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型',
defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性',
asec: '正序',
desc: '倒序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploading: '正在导入中',
uploadFailed: '导入失败,请稍后重试',
addPlugin: '新建plugin',
deletePlugin: '删除plugin',
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
autoDiscovery: '自动发现',
node: '节点',
adExecConfig: '执行配置',
adExecTarget: '执行机器',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
selectFromCMDBTips: '从CMDB中选择 ',
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点!',
username: '用户名',
password: '密码',
link: '链接',
list: '多值',
listTips: '字段的值是1个或者多个接口返回的值的类型是list',
computeForAllCITips: '所有CI触发计算',
confirmcomputeForAllCITips: '确认触发所有CI的计算',
isUnique: '是否唯一',
unique: '唯一',
isChoice: '是否选择',
defaultShow: '默认显示',
defaultShowTips: 'CI实例表格默认展示该字段',
isSortable: '可排序',
isIndex: '是否索引',
index: '索引',
indexTips: '字段可被用于检索,加速查询',
confirmDelete: '确认删除【{name}】?',
confirmDelete2: '确认删除?',
computeSuccess: '触发成功!',
basicConfig: '基础设置',
AttributeName: '属性名(英文)',
DataType: '数据类型',
defaultValue: '默认值',
autoIncID: '自增ID',
customTime: '自定义时间',
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
existedAttributes: '已有属性',
editAttribute: '编辑属性',
addAttributeTips1: '选中排序,则必须也要选中!',
uniqueConstraint: '唯一校验',
up: '上移',
down: '下移',
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
apply: '应用',
continueAdd: '继续添加',
filter: '过滤',
choiceOther: '其他模型属性',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!',
addRelation: '新增关系',
sourceCIType: '源模型',
sourceCITypeTips: '请选择源模型',
dstCIType: '目标模型名',
dstCITypeTips: '请选择目标模型',
relationType: '关联类型',
relationTypeTips: '请选择关联类型',
isParent: '被',
relationConstraint: '关系约束',
relationConstraintTips: '请选择关系约束',
one2Many: '一对多',
one2One: '一对一',
many2Many: '多对多',
basicInfo: '基本信息',
nameInputTips: '请输入名称',
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
changeInstance: '实例变更',
selectMutipleAttributes: '请选择属性(多选)',
selectSingleAttribute: '请选择属性(单选)',
beforeDays: '提前',
days: '天',
notifyAt: '发送时间',
notify: '通知',
triggerAction: '触发动作',
receivers: '收件人',
emailTips: '请输入邮箱,多个邮箱用;分隔',
customEmail: '自定义收件人',
notifySubject: '通知标题',
notifySubjectTips: '请输入通知标题',
notifyContent: '内容',
notifyMethod: '通知方式',
botSelect: '请选择机器人',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器',
editTriggerTitle: '编辑触发器 {name}',
newTriggerTitle: '新增触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
text: '文本',
datetime: '日期时间',
date: '日期',
time: '时间',
json: 'JSON',
event: '事件',
reg: '正则校验',
isInherit: '是否继承',
inheritType: '继承模型',
inheritTypePlaceholder: '请选择继承模型(多选)',
inheritFrom: '属性继承自{name}',
groupInheritFrom: '请至{name}进行修改'
},
components: {
unselectAttributes: '未选属性',
selectAttributes: '已选属性',
downloadCI: '导出数据',
filename: '文件名',
filenameInputTips: '请输入文件名',
saveType: '保存类型',
saveTypeTips: '请选择保存类型',
xlsx: 'Excel工作簿(*.xlsx)',
csv: 'CSV(逗号分隔)(*.csv)',
html: '网页(*.html)',
xml: 'XML数据(*.xml)',
txt: '文本文件(制表符分隔)(*.txt)',
grantUser: '授权用户/部门',
grantRole: '授权角色',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?',
readAttribute: '查看字段',
readCI: '查看实例',
config: '配置',
ciTypeGrant: '模型权限',
ciGrant: '实例权限',
attributeGrant: '字段权限',
relationGrant: '关系权限',
perm: '权限',
all: '全部',
customize: '自定义',
none: '无',
customizeFilterName: '请输入自定义筛选条件名',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
example: '示例值',
aliyun: '阿里云',
tencentcloud: '腾讯云',
huaweicloud: '华为云',
beforeChange: '变更前',
afterChange: '变更后',
noticeContentTips: '请输入通知内容',
saveQuery: '保存筛选条件',
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '已',
not: '未',
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
requestParam: '请求参数',
param: '参数{param}',
value: '值{value}',
clear: '清空',
},
batch: {
downloadFailed: '失败下载',
unselectCIType: '尚未选择模板类型',
pleaseUploadFile: '请上传文件',
batchUploadCanceled: '批量上传已取消',
selectCITypeTips: '请选择模板类型',
downloadTemplate: '下载模板',
drawTips: '点击或拖拽文件至此上传!',
supportFileTypes: '支持文件类型xlsxlsx',
uploadResult: '上传结果',
total: '共',
successItems: '条,已成功',
failedItems: '条,失败',
items: '条',
errorTips: '错误信息',
requestFailedTips: '请求出现错误,请稍后再试',
requestSuccessTips: '批量上传已完成',
},
preference: {
mySub: '我的订阅',
sub: '订阅',
cancelSub: '取消订阅',
editSub: '编辑订阅',
peopleSub: '位同事已订阅',
noSub: '暂无同事订阅',
cancelSubSuccess: '取消订阅成功',
confirmcancelSub: '确认取消订阅',
confirmcancelSub2: '确认取消订阅 {name} 吗?',
of: '的',
hoursAgo: '小时前',
daysAgo: '天前',
monthsAgo: '月前',
yearsAgo: '年前',
just: '刚刚',
},
custom_dashboard: {
charts: '图表',
newChart: '新增图表',
editChart: '编辑图表',
title: '标题',
titleTips: '请输入图表标题',
calcIndicators: '计算指标',
dimensions: '维度',
selectDimensions: '请选择维度',
quantity: '数量',
childCIType: '关系模型',
level: '层级',
levelTips: '请输入关系层级',
preview: '预览',
showIcon: '是否显示icon',
chartType: '图表类型',
dataFilter: '数据筛选',
format: '格式',
fontColor: '字体颜色',
backgroundColor: '背景颜色',
chartColor: '图表颜色',
chartLength: '图表长度',
barType: '柱状图类型',
stackedBar: '堆积柱状图',
multipleSeriesBar: '多系列柱状图',
axis: '轴',
direction: '方向',
lowerShadow: '下方阴影',
count: '指标',
bar: '柱状图',
line: '折线图',
pie: '饼状图',
table: '表格',
default: '默认',
relation: '关系',
noCustomDashboard: '管理员暂未定制仪表盘',
},
preference_relation: {
newServiceTree: '新增服务树',
serviceTreeName: '服务树名',
public: '公开',
saveLayout: '保存布局',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips1: '不能与当前选中节点形成视图,请重新选择!',
tips2: '请输入新增服务树名!',
tips3: '请选择至少两个节点!',
},
history: {
ciChange: 'CI变更',
relationChange: '关系变更',
ciTypeChange: '模型变更',
triggerHistory: '触发历史',
opreateTime: '操作时间',
user: '用户',
userTips: '输入筛选用户名',
filter: '筛选',
filterOperate: '筛选操作',
attribute: '属性',
old: '旧',
new: '新',
noUpdate: '没有修改',
itemsPerPage: '/页',
triggerName: '触发器名称',
event: '事件',
action: '动作',
status: '状态',
done: '已完成',
undone: '未完成',
triggerTime: '触发时间',
totalItems: '共 {total} 条记录',
pleaseSelect: '请选择',
startTime: '开始时间',
endTime: '结束时间',
deleteCIType: '删除模型',
addCIType: '新增模型',
updateCIType: '修改模型',
addAttribute: '新增属性',
updateAttribute: '修改属性',
deleteAttribute: '删除属性',
addTrigger: '新增触发器',
updateTrigger: '修改触发器',
deleteTrigger: '删除触发器',
addUniqueConstraint: '新增联合唯一',
updateUniqueConstraint: '修改联合唯一',
deleteUniqueConstraint: '删除联合唯一',
addRelation: '新增关系',
deleteRelation: '删除关系',
noModifications: '没有修改',
attr: '属性名',
attrId: '属性ID',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
},
relation_type: {
addRelationType: '新增关系类型',
nameTips: '请输入类型名',
},
ad: {
upload: '规则导入',
download: '规则导出',
accept: '入库',
acceptBy: '入库人',
acceptTime: '入库时间',
confirmAccept: '确认入库?',
acceptSuccess: '入库成功',
isAccept: '是否入库',
deleteADC: '确认删除该条数据?',
batchDelete: '确认删除这些数据?',
agent: '内置 & 插件',
snmp: '网络设备',
http: '公有云资源',
rule: '自动发现规则',
timeout: '超时错误',
mode: '模式',
collectSettings: '采集设置',
updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`,
server: '物理机',
vserver: '虚拟机',
nic: '网卡',
disk: '硬盘',
},
ci: {
attributeDesc: '属性说明',
selectRows: '选取:{rows} 项',
addRelation: '添加关系',
all: '全部',
batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress: '正在批量修改',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting: '正在删除...',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
confirmDeleteRelation: '确认删除关系?',
tips1: '多个值使用,分割',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
share: '分享',
noPermission: '暂无权限'
},
serviceTree: {
deleteNode: '删除节点',
tips1: '例q=os_version:centos&sort=os_version',
tips2: '表达式搜索',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh
const cmdb_zh = {
relation: '关系',
attribute: '属性',
menu: {
views: '视图',
config: '配置',
backend: '管理端',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
adCIs: '自动发现池',
preference: '我的订阅',
batchUpload: '批量导入',
citypeManage: '模型配置',
backendManage: '后台管理',
customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系',
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情'
},
ciType: {
ciType: '模型',
attributes: '模型属性',
relation: '模型关联',
trigger: '触发器',
attributeAD: '属性自动发现',
relationAD: '关系自动发现',
grant: '权限配置',
addGroup: '新增分组',
editGroup: '修改分组',
group: '分组',
attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型',
editGroupName: '编辑组名称',
deleteGroup: '删除该组',
CITypeName: '模型名(英文)',
English: '英文',
inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型',
defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性',
asec: '正序',
desc: '倒序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploading: '正在导入中',
uploadFailed: '导入失败,请稍后重试',
addPlugin: '新建plugin',
deletePlugin: '删除plugin',
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
autoDiscovery: '自动发现',
node: '节点',
adExecConfig: '执行配置',
adExecTarget: '执行机器',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
selectFromCMDBTips: '从CMDB中选择 ',
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点!',
username: '用户名',
password: '密码',
link: '链接',
list: '多值',
listTips: '字段的值是1个或者多个接口返回的值的类型是list',
computeForAllCITips: '所有CI触发计算',
confirmcomputeForAllCITips: '确认触发所有CI的计算',
isUnique: '是否唯一',
unique: '唯一',
isChoice: '是否选择',
defaultShow: '默认显示',
defaultShowTips: 'CI实例表格默认展示该字段',
isSortable: '可排序',
isIndex: '是否索引',
index: '索引',
indexTips: '字段可被用于检索,加速查询',
confirmDelete: '确认删除【{name}】?',
confirmDelete2: '确认删除?',
computeSuccess: '触发成功!',
basicConfig: '基础设置',
AttributeName: '属性名(英文)',
DataType: '数据类型',
defaultValue: '默认值',
autoIncID: '自增ID',
customTime: '自定义时间',
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
existedAttributes: '已有属性',
editAttribute: '编辑属性',
addAttributeTips1: '选中排序,则必须也要选中!',
uniqueConstraint: '唯一校验',
up: '上移',
down: '下移',
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
apply: '应用',
continueAdd: '继续添加',
filter: '过滤',
choiceOther: '其他模型属性',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!',
addRelation: '新增关系',
sourceCIType: '源模型',
sourceCITypeTips: '请选择源模型',
dstCIType: '目标模型名',
dstCITypeTips: '请选择目标模型',
relationType: '关联类型',
relationTypeTips: '请选择关联类型',
isParent: '被',
relationConstraint: '关系约束',
relationConstraintTips: '请选择关系约束',
one2Many: '一对多',
one2One: '一对一',
many2Many: '多对多',
basicInfo: '基本信息',
nameInputTips: '请输入名称',
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
changeInstance: '实例变更',
selectMutipleAttributes: '请选择属性(多选)',
selectSingleAttribute: '请选择属性(单选)',
beforeDays: '提前',
days: '天',
notifyAt: '发送时间',
notify: '通知',
triggerAction: '触发动作',
receivers: '收件人',
emailTips: '请输入邮箱,多个邮箱用;分隔',
customEmail: '自定义收件人',
notifySubject: '通知标题',
notifySubjectTips: '请输入通知标题',
notifyContent: '内容',
notifyMethod: '通知方式',
botSelect: '请选择机器人',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器',
editTriggerTitle: '编辑触发器 {name}',
newTriggerTitle: '新增触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
text: '文本',
datetime: '日期时间',
date: '日期',
time: '时间',
json: 'JSON',
event: '事件',
reg: '正则校验',
isInherit: '是否继承',
inheritType: '继承模型',
inheritTypePlaceholder: '请选择继承模型(多选)',
inheritFrom: '属性继承自{name}',
groupInheritFrom: '请至{name}进行修改'
},
components: {
unselectAttributes: '未选属性',
selectAttributes: '已选属性',
downloadCI: '导出数据',
filename: '文件名',
filenameInputTips: '请输入文件名',
saveType: '保存类型',
saveTypeTips: '请选择保存类型',
xlsx: 'Excel工作簿(*.xlsx)',
csv: 'CSV(逗号分隔)(*.csv)',
html: '网页(*.html)',
xml: 'XML数据(*.xml)',
txt: '文本文件(制表符分隔)(*.txt)',
grantUser: '授权用户/部门',
grantRole: '授权角色',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?',
readAttribute: '查看字段',
readCI: '查看实例',
config: '配置',
ciTypeGrant: '模型权限',
ciGrant: '实例权限',
attributeGrant: '字段权限',
relationGrant: '关系权限',
perm: '权限',
all: '全部',
customize: '自定义',
none: '无',
customizeFilterName: '请输入自定义筛选条件名',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
example: '示例值',
aliyun: '阿里云',
tencentcloud: '腾讯云',
huaweicloud: '华为云',
beforeChange: '变更前',
afterChange: '变更后',
noticeContentTips: '请输入通知内容',
saveQuery: '保存筛选条件',
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '已',
not: '未',
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
requestParam: '请求参数',
param: '参数{param}',
value: '值{value}',
clear: '清空',
},
batch: {
downloadFailed: '失败下载',
unselectCIType: '尚未选择模板类型',
pleaseUploadFile: '请上传文件',
batchUploadCanceled: '批量上传已取消',
selectCITypeTips: '请选择模板类型',
downloadTemplate: '下载模板',
drawTips: '点击或拖拽文件至此上传!',
supportFileTypes: '支持文件类型xlsxlsx',
uploadResult: '上传结果',
total: '共',
successItems: '条,已成功',
failedItems: '条,失败',
items: '条',
errorTips: '错误信息',
requestFailedTips: '请求出现错误,请稍后再试',
requestSuccessTips: '批量上传已完成',
},
preference: {
mySub: '我的订阅',
sub: '订阅',
cancelSub: '取消订阅',
editSub: '编辑订阅',
peopleSub: '位同事已订阅',
noSub: '暂无同事订阅',
cancelSubSuccess: '取消订阅成功',
confirmcancelSub: '确认取消订阅',
confirmcancelSub2: '确认取消订阅 {name} 吗?',
of: '的',
hoursAgo: '小时前',
daysAgo: '天前',
monthsAgo: '月前',
yearsAgo: '年前',
just: '刚刚',
},
custom_dashboard: {
charts: '图表',
newChart: '新增图表',
editChart: '编辑图表',
title: '标题',
titleTips: '请输入图表标题',
calcIndicators: '计算指标',
dimensions: '维度',
selectDimensions: '请选择维度',
quantity: '数量',
childCIType: '关系模型',
level: '层级',
levelTips: '请输入关系层级',
preview: '预览',
showIcon: '是否显示icon',
chartType: '图表类型',
dataFilter: '数据筛选',
format: '格式',
fontColor: '字体颜色',
backgroundColor: '背景颜色',
chartColor: '图表颜色',
chartLength: '图表长度',
barType: '柱状图类型',
stackedBar: '堆积柱状图',
multipleSeriesBar: '多系列柱状图',
axis: '轴',
direction: '方向',
lowerShadow: '下方阴影',
count: '指标',
bar: '柱状图',
line: '折线图',
pie: '饼状图',
table: '表格',
default: '默认',
relation: '关系',
noCustomDashboard: '管理员暂未定制仪表盘',
},
preference_relation: {
newServiceTree: '新增服务树',
serviceTreeName: '服务树名',
public: '公开',
saveLayout: '保存布局',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips1: '不能与当前选中节点形成视图,请重新选择!',
tips2: '请输入新增服务树名!',
tips3: '请选择至少两个节点!',
},
history: {
ciChange: 'CI变更',
relationChange: '关系变更',
ciTypeChange: '模型变更',
triggerHistory: '触发历史',
opreateTime: '操作时间',
user: '用户',
userTips: '输入筛选用户名',
filter: '筛选',
filterOperate: '筛选操作',
attribute: '属性',
old: '旧',
new: '新',
noUpdate: '没有修改',
itemsPerPage: '/页',
triggerName: '触发器名称',
event: '事件',
action: '动作',
status: '状态',
done: '已完成',
undone: '未完成',
triggerTime: '触发时间',
totalItems: '共 {total} 条记录',
pleaseSelect: '请选择',
startTime: '开始时间',
endTime: '结束时间',
deleteCIType: '删除模型',
addCIType: '新增模型',
updateCIType: '修改模型',
addAttribute: '新增属性',
updateAttribute: '修改属性',
deleteAttribute: '删除属性',
addTrigger: '新增触发器',
updateTrigger: '修改触发器',
deleteTrigger: '删除触发器',
addUniqueConstraint: '新增联合唯一',
updateUniqueConstraint: '修改联合唯一',
deleteUniqueConstraint: '删除联合唯一',
addRelation: '新增关系',
deleteRelation: '删除关系',
noModifications: '没有修改',
attr: '属性名',
attrId: '属性ID',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
},
relation_type: {
addRelationType: '新增关系类型',
nameTips: '请输入类型名',
},
ad: {
upload: '规则导入',
download: '规则导出',
accept: '入库',
acceptBy: '入库人',
acceptTime: '入库时间',
confirmAccept: '确认入库?',
acceptSuccess: '入库成功',
isAccept: '是否入库',
deleteADC: '确认删除该条数据?',
batchDelete: '确认删除这些数据?',
agent: '内置 & 插件',
snmp: '网络设备',
http: '公有云资源',
rule: '自动发现规则',
timeout: '超时错误',
mode: '模式',
collectSettings: '采集设置',
updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`,
server: '物理机',
vserver: '虚拟机',
nic: '网卡',
disk: '硬盘',
},
ci: {
attributeDesc: '属性说明',
selectRows: '选取:{rows} 项',
addRelation: '添加关系',
all: '全部',
batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress: '正在批量修改',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting: '正在删除...',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
confirmDeleteRelation: '确认删除关系?',
tips1: '多个值使用,分割',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
share: '分享',
noPermission: '暂无权限'
},
serviceTree: {
deleteNode: '删除节点',
tips1: '例q=os_version:centos&sort=os_version',
tips2: '表达式搜索',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh

View File

@ -1,394 +1,430 @@
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation')
}}</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input
:placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value"
style="width: 50%"
/>
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
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'))
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
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) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return this.valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
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>
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation')
}}</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input
:placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value"
style="width: 50%"
/>
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
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'))
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
// this.form.validateFields((err, values) => {
// if (err) {
// _this.$message.error('字段填写不符合要求!')
// return
// }
// Object.keys(values).forEach((k) => {
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
// }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined
// }
// })
// if (_this.action === 'update') {
// _this.$emit('submit', values)
// return
// }
// values.ci_type = _this.typeId
// console.log(values)
// this.attributesByGroup.forEach((group) => {
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// })
// console.log(1111)
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!')
// // _this.visible = false
// // _this.$emit('reload')
// // })
// })
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
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) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return this.valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
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>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
height="auto"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 3, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></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>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
return {
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
height="auto"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 3, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></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>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
return {
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</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>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<!-- <a-input
v-model="expression"
class="ci-searchform-expression"
:style="{ width, marginBottom: '10px' }"
:placeholder="placeholder"
@focus="
() => {
isFocusExpression = true
}
"
/> -->
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
/>
<!-- <a @click="handleSearch"><a-icon type="search"/></a> -->
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<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>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
</a-modal>
</template>
<script>
/* eslint-disable no-useless-escape */
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue'
export default {
name: 'AddTableModal',
components: { SearchForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
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)
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>
<template>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<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>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal>
</template>
<script>
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'AddTableModal',
components: { SearchForm, CreateInstanceForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
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>
<a-dropdown :trigger="['contextmenu']">
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
<a-menu-item v-for="item in menuList" :key="item.id">{{ $t('new') }} {{ item.alias }}</a-menu-item>
<a-menu-item v-if="showDelete" key="delete">{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item>
</a-menu>
<div
:style="{
width: '100%',
display: 'inline-flex',
justifyContent: 'space-between',
alignItems: 'center',
}"
@click="clickNode"
>
<span
:style="{
display: 'flex',
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
</template>
<span
:style="{
display: 'inline-block',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: '#d3d3d3',
color: '#fff',
textAlign: 'center',
lineHeight: '16px',
fontSize: '12px',
}"
v-else
>{{ ciTypeName ? ciTypeName[0].toUpperCase() : 'i' }}</span
>
<span :style="{ marginLeft: '5px' }">{{ this.title }}</span>
</span>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div>
</a-dropdown>
</template>
<script>
export default {
name: 'ContextMenu',
props: {
title: {
type: String,
default: '',
},
treeKey: {
type: String,
default: '',
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
isLeaf: {
type: Boolean,
default: () => false,
},
ciTypes: {
type: Array,
default: () => [],
},
},
data() {
return {
switchIcon: 'down',
}
},
computed: {
childLength() {
const reg = /(?<=\()\S+(?=\))/g
return Number(this.title.match(reg)[0])
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
return { id: item.id, alias: item.alias || item.name }
})
}
return _menuList
},
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.icon || null
},
ciTypeName() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.name || ''
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
},
}
</script>
<style></style>
<template>
<div
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span>
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
v-if="icon.includes('$$') && icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else-if="icon.includes('$$') && icon.split('$$')[0]"
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
<span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
</template>
<span class="relation-views-node-title">{{ this.title }}</span>
</span>
<a-dropdown>
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
<template v-if="showBatchLevel === null">
<a-menu-item
v-for="item in menuList"
:key="item.id"
><a-icon type="plus-circle" />{{ $t('new') }} {{ item.alias }}</a-menu-item
>
<a-menu-item
v-if="showDelete"
key="delete"
><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item
>
<a-menu-divider />
<a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
<a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
<a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
<a-menu-divider />
<a-menu-item
key="batch"
><ops-icon type="icon-xianxing-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item
>
</template>
<template v-else>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchGrant"
><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchRevoke"
><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<a-menu-divider />
<template v-if="showBatchLevel > 0">
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchDelete"
><ops-icon type="icon-xianxing-delete" />{{ $t('delete') }}</a-menu-item
>
<a-menu-divider />
</template>
<a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
</template>
</a-menu>
<a-icon class="relation-views-node-operation" type="ellipsis" />
</a-dropdown>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div>
</template>
<script>
export default {
name: 'ContextMenu',
props: {
title: {
type: String,
default: '',
},
treeKey: {
type: String,
default: '',
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
isLeaf: {
type: Boolean,
default: () => false,
},
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
},
data() {
return {
switchIcon: 'down',
}
},
computed: {
childLength() {
const reg = /(?<=\()\S+(?=\))/g
return Number(this.title.match(reg)[0])
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
return { id: item.id, alias: item.alias || item.name }
})
}
return _menuList
},
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>
<treeselect
:disable-branch-nodes="multiple ? false : true"
:multiple="multiple"
:options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue"
:max-height="200"
:noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="20"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
>
</treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect',
},
placeholder: {
type: String,
default: '',
},
idType: {
type: Number,
default: 1,
},
departmentKey: {
type: String,
default: 'department_id',
},
employeeKey: {
type: String,
default: 'employee_id',
},
},
data() {
return {}
},
inject: {
provide_allTreeDepAndEmp: {
from: 'provide_allTreeDepAndEmp',
},
readOnly: {
from: 'readOnly',
default: false,
},
},
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
},
},
methods: {},
}
</script>
<style scoped></style>
<template>
<treeselect
:disable-branch-nodes="multiple ? false : true"
:multiple="multiple"
:options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue"
:max-height="200"
:noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="limit"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
:flat="flat"
>
</treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect',
},
placeholder: {
type: String,
default: '',
},
idType: {
type: Number,
default: 1,
},
departmentKey: {
type: String,
default: 'department_id',
},
employeeKey: {
type: String,
default: 'employee_id',
},
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
},
data() {
return {}
},
inject: {
provide_allTreeDepAndEmp: {
from: 'provide_allTreeDepAndEmp',
},
readOnly: {
from: 'readOnly',
default: false,
},
},
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
},
},
methods: {},
}
</script>
<style scoped></style>