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

@ -20,6 +20,7 @@
} }
} }
" "
:disabled="disabled"
> >
</treeselect> </treeselect>
</div> </div>
@ -42,6 +43,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
<div <div
:title="node.label" :title="node.label"
@ -80,6 +82,7 @@
@select="(value) => handleChangeExp(value, item, index)" @select="(value) => handleChangeExp(value, item, index)"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
</treeselect> </treeselect>
<treeselect <treeselect
@ -103,6 +106,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
<div <div
:title="node.label" :title="node.label"
@ -125,6 +129,7 @@
v-model="item.min" v-model="item.min"
:style="{ width: '78px' }" :style="{ width: '78px' }"
:placeholder="$t('min')" :placeholder="$t('min')"
:disabled="disabled"
/> />
~ ~
<a-input <a-input
@ -133,6 +138,7 @@
v-model="item.max" v-model="item.max"
:style="{ width: '78px' }" :style="{ width: '78px' }"
:placeholder="$t('max')" :placeholder="$t('max')"
:disabled="disabled"
/> />
</a-input-group> </a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
@ -155,6 +161,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
</treeselect> </treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
@ -166,8 +173,10 @@
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" :placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input" class="ops-input"
:style="{ width: '175px' }" :style="{ width: '175px' }"
:disabled="disabled"
></a-input> ></a-input>
<div v-else :style="{ width: '175px' }"></div> <div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')"> <a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip> </a-tooltip>
@ -177,8 +186,9 @@
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> <a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> <a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip> </a-tooltip>
</template>
</a-space> </a-space>
<div class="table-filter-add"> <div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a> <a @click="handleAddRule">+ {{ $t('new') }}</a>
</div> </div>
</div> </div>
@ -211,6 +221,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {

View File

@ -16,6 +16,7 @@
:needAddHere="needAddHere" :needAddHere="needAddHere"
v-model="ruleList" v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/> />
<a-divider :style="{ margin: '10px 0' }" /> <a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px"> <div style="width:554px">
@ -31,6 +32,7 @@
v-else v-else
v-model="ruleList" v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/> />
</div> </div>
</template> </template>
@ -69,6 +71,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {

View File

@ -113,6 +113,10 @@ export default {
}, },
mounted() { mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`) this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) { if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'

View File

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

View File

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

View File

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

View File

@ -14,9 +14,16 @@
<script> <script>
import EmployeeTransfer from '@/components/EmployeeTransfer' import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer' import RoleTransfer from '@/components/RoleTransfer'
export default { export default {
name: 'GrantModal', name: 'GrantModal',
components: { EmployeeTransfer, RoleTransfer }, components: { EmployeeTransfer, RoleTransfer },
props: {
customTitle: {
type: String,
default: '',
},
},
data() { data() {
return { return {
visible: false, visible: false,
@ -25,6 +32,9 @@ export default {
}, },
computed: { computed: {
title() { title() {
if (this.customTitle) {
return this.customTitle
}
if (this.type === 'depart') { if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser') return this.$t('cmdb.components.grantUser')
} }

View File

@ -6,7 +6,8 @@
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' }, { value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') }, { value: 3, label: $t('cmdb.components.none') },
]" ]"
v-model="radioValue" :value="radioValue"
@change="changeRadioValue"
> >
<template slot="extra_2" v-if="radioValue === 2"> <template slot="extra_2" v-if="radioValue === 2">
<treeselect <treeselect
@ -128,6 +129,9 @@ export default {
this.visible = true this.visible = true
this.colType = colType this.colType = colType
this.row = row this.row = row
this.form = {
name: '',
}
if (this.colType === 'read_ci') { if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => { await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6') this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
@ -149,10 +153,6 @@ export default {
}) })
} }
} }
} else {
this.form = {
name: '',
}
} }
}, },
async handleOk() { async handleOk() {
@ -198,6 +198,13 @@ export default {
} }
this.expression = expression this.expression = expression
}, },
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
}, },
} }
</script> </script>

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

@ -52,7 +52,7 @@
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }" :style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }"
@click="emitRefresh" @click="emitRefresh"
/> />
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px' }"> <a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title"> <template slot="title">
{{ $t('cmdb.components.ciSearchTips') }} {{ $t('cmdb.components.ciSearchTips') }}
</template> </template>
@ -97,6 +97,7 @@
</a-space> </a-space>
</div> </div>
<a-space> <a-space>
<slot name="extraContent"></slot>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button> <a-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'"> <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a <a
@ -191,6 +192,9 @@ export default {
} }
}, },
methods: { methods: {
// toggleAdvanced() {
// this.advanced = !this.advanced
// },
getCITypeGroups() { getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => { getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res this.ciTypeGroup = res

View File

@ -46,8 +46,9 @@ const cmdb_en = {
selectDefaultOrderAttr: 'Select default sorting attributes', selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order', asec: 'Forward order',
desc: 'Reverse order', desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies', uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier', uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
notfound: 'Can\'t find what you want?', notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@ -223,7 +224,7 @@ const cmdb_en = {
pleaseSearch: 'Please search', pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering', conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description', 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', 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*', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType', subCIType: 'Subscription CIType',
already: 'already', already: 'already',
@ -466,7 +467,7 @@ const cmdb_en = {
tips3: 'Please select the fields that need to be modified', tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected', tips4: 'At least one field must be selected',
tips5: 'Search name | alias', 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', 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', 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', tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only', tips9: 'For front-end only',
@ -483,6 +484,16 @@ const cmdb_en = {
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!', alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed', copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?', 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: { tree: {
tips1: 'Please go to Preference page first to complete your subscription!', tips1: 'Please go to Preference page first to complete your subscription!',

View File

@ -48,6 +48,7 @@ const cmdb_zh = {
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?', notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@ -223,7 +224,7 @@ const cmdb_zh = {
pleaseSearch: '请查找', pleaseSearch: '请查找',
conditionFilter: '条件过滤', conditionFilter: '条件过滤',
attributeDesc: '属性说明', attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤', ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型', subCIType: '订阅模型',
already: '已', already: '已',
@ -465,7 +466,7 @@ const cmdb_zh = {
tips3: '请选择需要修改的字段', tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段', tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名', tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP', tips8: '多值, 比如内网IP',
tips9: '仅针对前端', tips9: '仅针对前端',
@ -482,6 +483,16 @@ const cmdb_zh = {
alert1: '管理员 还未配置业务关系, 或者你无权限访问!', alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败', copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?', deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
}, },
tree: { tree: {
tips1: '请先到 我的订阅 页面完成订阅!', tips1: '请先到 我的订阅 页面完成订阅!',

View File

@ -220,6 +220,7 @@ export default {
if (otherGroupAttr.length) { if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr }) _attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
} }
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup this.attributesByGroup = _attributesByGroup
}) })
}, },
@ -296,6 +297,38 @@ export default {
_this.$emit('reload', { ci_id: res.ci_id }) _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() { handleClose() {
this.visible = false this.visible = false
@ -363,6 +396,9 @@ export default {
this.batchUpdateLists.splice(_idx, 1) this.batchUpdateLists.splice(_idx, 1)
} }
}, },
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) { handleFocusInput(e, attr) {
console.log(attr) console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name) const _tempFind = this.attributeList.find((item) => item.name === attr.name)

View File

@ -27,7 +27,7 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_2"> <a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span> <span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px' }"> <div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" /> <CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div> </div>
</a-tab-pane> </a-tab-pane>

View File

@ -270,7 +270,17 @@
</div> </div>
</el-select> </el-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.ciType.uniqueKey')"> <a-form-item>
<template slot="label">
<a-tooltip :title="$t('cmdb.ciType.uniqueKeyTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
/>
</a-tooltip>
<span>{{ $t('cmdb.ciType.uniqueKey') }}</span>
</template>
<el-select <el-select
size="small" size="small"
filterable filterable

View File

@ -2,14 +2,55 @@
<div :style="{ marginBottom: '-24px', overflow: 'hidden' }"> <div :style="{ marginBottom: '-24px', overflow: 'hidden' }">
<div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper"> <div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper">
<div class="cmdb-views-header"> <div class="cmdb-views-header">
<span class="cmdb-views-header-title">{{ $route.meta.name }}</span> <span
class="cmdb-views-header-title"
>{{ $route.meta.name }}
<div
class="ops-list-batch-action"
:style="{ backgroundColor: '#c0ceeb' }"
v-if="showBatchLevel !== null && batchTreeKey && batchTreeKey.length"
>
<span
@click="
() => {
$refs.grantModal.open('depart')
}
"
>{{ $t('grant') }}</span
>
<a-divider type="vertical" />
<span
@click="
() => {
$refs.revokeModal.open()
}
"
>{{ $t('revoke') }}</span
>
<template v-if="showBatchLevel > 0">
<a-divider type="vertical" />
<span @click="batchDeleteCIRelationFromTree">{{ $t('delete') }}</span>
</template>
<a-divider type="vertical" />
<span
@click="
() => {
showBatchLevel = null
batchTreeKey = []
}
"
>{{ $t('cancel') }}</span
>
<span>{{ $t('selectRows', { rows: batchTreeKey.length }) }}</span>
</div>
</span>
<a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button> <a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button>
</div> </div>
<SplitPane <SplitPane
:min="200" :min="200"
:max="500" :max="500"
:paneLengthPixel.sync="paneLengthPixel" :paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-relation-views" :appName="`cmdb-relation-views-${viewId}`"
triggerColor="#F0F5FF" triggerColor="#F0F5FF"
:triggerLength="18" :triggerLength="18"
> >
@ -24,7 +65,6 @@
@drop="onDrop" @drop="onDrop"
:expandedKeys="expandedKeys" :expandedKeys="expandedKeys"
> >
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }"> <template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu <ContextMenu
:title="title" :title="title"
@ -35,7 +75,10 @@
:id2type="relationViews.id2type" :id2type="relationViews.id2type"
@onContextMenuClick="onContextMenuClick" @onContextMenuClick="onContextMenuClick"
@onNodeClick="onNodeClick" @onNodeClick="onNodeClick"
:ciTypes="ciTypes" :ciTypeIcons="ciTypeIcons"
:showBatchLevel="showBatchLevel"
:batchTreeKey="batchTreeKey"
@clickCheckbox="clickCheckbox"
/> />
</template> </template>
</a-tree> </a-tree>
@ -313,9 +356,8 @@
v-else-if="relationViews.name2id && !relationViews.name2id.length" v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert> ></a-alert>
<AddTableModal ref="addTableModal" @reload="reload" /> <AddTableModal ref="addTableModal" @reload="reload" />
<!-- <GrantDrawer ref="grantDrawer" resourceTypeName="RelationView" app_id="cmdb" /> -->
<CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" /> <CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" />
<GrantModal ref="grantModal" @handleOk="onRelationViewGrant" :customTitle="$t('cmdb.serviceTree.grantTitle')" />
<CiDetailDrawer ref="detail" :typeId="Number(currentTypeId[0])" /> <CiDetailDrawer ref="detail" :typeId="Number(currentTypeId[0])" />
<create-instance-form <create-instance-form
ref="create" ref="create"
@ -325,11 +367,12 @@
/> />
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" /> <BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ReadPermissionsModal ref="readPermissionsModal" />
<RevokeModal ref="revokeModal" @handleRevoke="handleRevoke" />
</div> </div>
</template> </template>
<script> <script>
/* eslint-disable no-useless-escape */
import _ from 'lodash' import _ from 'lodash'
import { Tree } from 'element-ui' import { Tree } from 'element-ui'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
@ -349,7 +392,7 @@ import {
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci' import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '../../api/CIType' import { getCITypeIcons, grantCiType, revokeCiType } from '../../api/CIType'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission' import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource' import { searchResourceType } from '@/modules/acl/api/resource'
import SplitPane from '@/components/SplitPane' import SplitPane from '@/components/SplitPane'
@ -361,8 +404,11 @@ import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PasswordField from '../../components/passwordField/index.vue' import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue' import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant' import CMDBGrant from '../../components/cmdbGrant'
import GrantModal from '../../components/cmdbGrant/grantModal.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons' import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr' import { getAttrPassword } from '../../api/CITypeAttr'
import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
export default { export default {
name: 'RelationViews', name: 'RelationViews',
@ -370,8 +416,8 @@ export default {
SearchForm, SearchForm,
AddTableModal, AddTableModal,
ContextMenu, ContextMenu,
// GrantDrawer,
CMDBGrant, CMDBGrant,
GrantModal,
SplitPane, SplitPane,
ElTree: Tree, ElTree: Tree,
EditAttrsPopover, EditAttrsPopover,
@ -382,13 +428,15 @@ export default {
PasswordField, PasswordField,
PreferenceSearch, PreferenceSearch,
OpsMoveIcon, OpsMoveIcon,
ReadPermissionsModal,
RevokeModal,
}, },
data() { data() {
return { return {
treeData: [], treeData: [],
triggerSelect: false, triggerSelect: false,
treeNode: null, treeNode: null,
ciTypes: [], ciTypeIcons: {},
relationViews: {}, relationViews: {},
levels: [], levels: [],
showTypeIds: [], showTypeIds: [],
@ -430,6 +478,10 @@ export default {
passwordValue: {}, passwordValue: {},
lastEditCiId: null, lastEditCiId: null,
isContinueCloseEdit: true, isContinueCloseEdit: true,
contextMenuKey: null,
showBatchLevel: null,
batchTreeKey: [],
} }
}, },
@ -452,6 +504,21 @@ export default {
isShowBatchIcon() { isShowBatchIcon() {
return !!this.selectedRowKeys.length return !!this.selectedRowKeys.length
}, },
topo_flatten() {
return this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
},
descendant_ids() {
return this.topo_flatten.slice(this.treeKeys.length).join(',')
},
descendant_ids_for_statistics() {
return this.topo_flatten.slice(this.treeKeys.length + 1).join(',')
},
root_parent_path() {
return this.treeKeys
.slice(0, this.treeKeys.length)
.map((item) => item.split('%')[0])
.join(',')
},
}, },
provide() { provide() {
return { return {
@ -505,8 +572,8 @@ export default {
}) })
}, },
getCITypesList() { getCITypesList() {
getCITypes().then((res) => { getCITypeIcons().then((res) => {
this.ciTypes = res.ci_types this.ciTypeIcons = res
}) })
}, },
refreshTable() { refreshTable() {
@ -572,33 +639,38 @@ export default {
q = q.slice(1) q = q.slice(1)
} }
if (this.treeKeys.length === 0) { if (this.treeKeys.length === 0) {
await this.judgeCITypes(q) // await this.judgeCITypes(q)
if (!refreshType) { if (!refreshType) {
this.loadRoot() await this.loadRoot()
} }
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || '' // const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) { // if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q // q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
} else { // } else {
q = `q=_type:${this.currentTypeId[0]},` + q // q = `q=_type:${this.currentTypeId[0]},` + q
} // }
if (this.currentTypeId[0]) { // if (this.currentTypeId[0] && this.treeData && this.treeData.length) {
const res = await searchCI2(q) // // default select first node
this.pageNo = res.page // this.onNodeClick(this.treeData[0].key)
this.numfound = res.numfound // const res = await searchCI2(q)
res.result.forEach((item, index) => (item.key = item._id)) // const root_id = this.treeData.map((item) => item.id).join(',')
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6') // q += `&root_id=${root_id}`
console.log(jsonAttrList)
this.instanceList = res['result'].map((item) => { // this.pageNo = res.page
jsonAttrList.forEach( // this.numfound = res.numfound
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '') // res.result.forEach((item, index) => (item.key = item._id))
) // const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
return { ..._.cloneDeep(item) } // console.log(jsonAttrList)
}) // this.instanceList = res['result'].map((item) => {
this.initialInstanceList = _.cloneDeep(this.instanceList) // jsonAttrList.forEach(
this.calcColumns() // (jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
} // )
// return { ..._.cloneDeep(item) }
// })
// this.initialInstanceList = _.cloneDeep(this.instanceList)
// this.calcColumns()
// }
} else { } else {
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}` q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}`
@ -634,10 +706,10 @@ export default {
level = [1] level = [1]
} }
q += `&level=${level.join(',')}` q += `&level=${level.join(',')}`
await this.judgeCITypes(q)
if (!refreshType) { if (!refreshType) {
this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level) this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level)
} }
await this.judgeCITypes(q)
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || '' const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) { if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
@ -645,8 +717,12 @@ export default {
q = `q=_type:${this.currentTypeId[0]},` + q q = `q=_type:${this.currentTypeId[0]},` + q
} }
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1` q = q + `&has_m2m=1`
} }
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
if (this.currentTypeId[0]) { if (this.currentTypeId[0]) {
const res = await searchCIRelation(q) const res = await searchCIRelation(q)
@ -666,7 +742,6 @@ export default {
this.calcColumns() this.calcColumns()
} }
if (refreshType === 'refreshNumber') { if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => { const promises = this.treeKeys.map((key, index) => {
let ancestor_ids let ancestor_ids
@ -684,8 +759,9 @@ export default {
ancestor_ids, ancestor_ids,
root_ids: key.split('%')[0], root_ids: key.split('%')[0],
level: this.treeKeys.length - index, level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((res) => { }).then((res) => {
let result let result
const getTreeItem = (data, id) => { const getTreeItem = (data, id) => {
@ -741,22 +817,25 @@ export default {
const promises = _showTypeIds.map((typeId) => { const promises = _showTypeIds.map((typeId) => {
let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1') let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
_q = _q + `&&has_m2m=1` _q = _q + `&has_m2m=1`
} }
console.log(_q) if (this.root_parent_path) {
if (this.treeKeys.length === 0) { _q = _q + `&root_parent_path=${this.root_parent_path}`
return searchCI2(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
} }
}) // if (this.treeKeys.length === 0) {
} else { // return searchCI2(_q).then((res) => {
// if (res.numfound !== 0) {
// showTypeIds.push(typeId)
// }
// })
// } else {
_q = _q + `&descendant_ids=${this.descendant_ids}`
return searchCIRelation(_q).then((res) => { return searchCIRelation(_q).then((res) => {
if (res.numfound !== 0) { if (res.numfound !== 0) {
showTypeIds.push(typeId) showTypeIds.push(typeId)
} }
}) })
} // }
}) })
await Promise.all(promises).then(async () => { await Promise.all(promises).then(async () => {
if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) { if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) {
@ -780,7 +859,7 @@ export default {
}, },
async loadRoot() { async loadRoot() {
searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000`).then(async (res) => { await searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000&use_id_filter=1`).then(async (res) => {
const facet = [] const facet = []
const ciIds = [] const ciIds = []
res.result.forEach((item) => { res.result.forEach((item) => {
@ -797,8 +876,9 @@ export default {
return statisticsCIRelation({ return statisticsCIRelation({
root_ids: ciIds.join(','), root_ids: ciIds.join(','),
level: level, level: level,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => { }).then((num) => {
facet.forEach((item, idx) => { facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + ''] item[1] += num[ciIds[idx] + '']
@ -806,16 +886,17 @@ export default {
}) })
}) })
await Promise.all(promises) await Promise.all(promises)
this.wrapTreeData(facet, 'loadRoot') this.wrapTreeData(facet)
// default select first node
this.onNodeClick(this.treeData[0].key)
}) })
}, },
async loadNoRoot(rootIdAndTypeId, level) { async loadNoRoot(rootIdAndTypeId, level) {
const rootId = rootIdAndTypeId.split('%')[0] const rootId = rootIdAndTypeId.split('%')[0]
const typeId = Number(rootIdAndTypeId.split('%')[1]) const typeId = Number(rootIdAndTypeId.split('%')[1])
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? [] const index = this.topo_flatten.findIndex((id) => id === typeId)
const index = topo_flatten.findIndex((id) => id === typeId) const _type = this.topo_flatten[index + 1]
const _type = topo_flatten[index + 1]
if (_type) { if (_type) {
let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000` let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000`
if ( if (
@ -829,8 +910,12 @@ export default {
.join(',')}` .join(',')}`
} }
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1` q = q + `&has_m2m=1`
} }
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
searchCIRelation(q).then(async (res) => { searchCIRelation(q).then(async (res) => {
const facet = [] const facet = []
const ciIds = [] const ciIds = []
@ -852,8 +937,9 @@ export default {
ancestor_ids, ancestor_ids,
root_ids: ciIds.join(','), root_ids: ciIds.join(','),
level: _level - 1, level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => { }).then((num) => {
facet.forEach((item, idx) => { facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + ''] item[1] += num[ciIds[idx] + '']
@ -862,7 +948,7 @@ export default {
} }
}) })
await Promise.all(promises) await Promise.all(promises)
this.wrapTreeData(facet, 'loadNoRoot') this.wrapTreeData(facet)
}) })
} }
}, },
@ -917,6 +1003,7 @@ export default {
} }
this.treeKeys = treeNode.eventKey.split('@^@').filter((item) => item !== '') this.treeKeys = treeNode.eventKey.split('@^@').filter((item) => item !== '')
this.treeNode = treeNode this.treeNode = treeNode
// this.refreshTable()
resolve() resolve()
}) })
}, },
@ -979,8 +1066,7 @@ export default {
this.$refs.xTable.refreshColumn() this.$refs.xTable.refreshColumn()
}) })
}, },
onContextMenuClick(treeKey, menuKey) { calculateParamsFromTreeKey(treeKey, menuKey) {
if (treeKey) {
const splitTreeKey = treeKey.split('@^@') const splitTreeKey = treeKey.split('@^@')
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%') const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%')
const firstCIObj = JSON.parse(_tempTree[2]) const firstCIObj = JSON.parse(_tempTree[2])
@ -996,10 +1082,21 @@ export default {
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1) .slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',') ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',')
} }
return { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids }
},
onContextMenuClick(treeKey, menuKey) {
if (treeKey) {
if (!['batchGrant', 'batchRevoke', 'batchDelete', 'batchCancel'].includes(menuKey)) {
this.contextMenuKey = treeKey
}
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = this.calculateParamsFromTreeKey(
treeKey,
menuKey
)
if (menuKey === 'delete') { if (menuKey === 'delete') {
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%') const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
const that = this const that = this
this.$confirm({ this.$confirm({
title: that.$t('warning'), title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>, content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>,
@ -1012,6 +1109,24 @@ export default {
}) })
}, },
}) })
} else if (menuKey === 'grant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'revoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'view') {
this.$refs.readPermissionsModal.open(treeKey)
} else if (menuKey === 'batch') {
this.showBatchLevel = splitTreeKey.filter((item) => !!item).length - 1
this.batchTreeKey = []
} else if (menuKey === 'batchGrant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'batchRevoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'batchDelete') {
this.batchDeleteCIRelationFromTree()
} else if (menuKey === 'batchCancel') {
this.showBatchLevel = null
this.batchTreeKey = []
} else { } else {
const childTypeId = menuKey const childTypeId = menuKey
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids) this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
@ -1066,8 +1181,10 @@ export default {
const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '') const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '')
if (_splitDragKey.length - 1 === _splitTargetKey.length) { if (_splitDragKey.length - 1 === _splitTargetKey.length) {
const dragId = _splitDragKey[_splitDragKey.length - 1].split('%')[0] const dragId = _splitDragKey[_splitDragKey.length - 1].split('%')[0]
// const targetObj = JSON.parse(_splitTargetKey[_splitTargetKey.length - 1].split('%')[2])
const targetId = _splitTargetKey[_splitTargetKey.length - 1].split('%')[0] const targetId = _splitTargetKey[_splitTargetKey.length - 1].split('%')[0]
console.log(_splitDragKey) console.log(_splitDragKey)
// TODO 拖拽这里不造咋弄 等等再说吧
batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => { batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => {
this.reload() this.reload()
}) })
@ -1438,6 +1555,138 @@ export default {
this.$message.error(this.$t('cmdb.serviceTreecopyFailed')) this.$message.error(this.$t('cmdb.serviceTreecopyFailed'))
}) })
}, },
async onRelationViewGrant({ department, user }, type) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
await this.relationViewGrant({ department, user }, this.batchTreeKey[i], (_result) => {
result.push(..._result)
})
}
this.showBatchLevel = null
this.batchTreeKey = []
} else {
await this.relationViewGrant({ department, user }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
},
async relationViewGrant({ department, user }, nodeKey, callback) {
const needGrantNodes = nodeKey
.split('@^@')
.filter((item) => !!item)
.reverse()
console.log(needGrantNodes)
const needGrantRids = [...department, ...user]
const floor = Math.ceil(needGrantRids.length / 6)
const result = []
for (let i = 0; i < needGrantNodes.length; i++) {
const grantNode = needGrantNodes[i]
const _grantNode = grantNode.split('%')
const ciId = _grantNode[0]
const typeId = _grantNode[1]
const uniqueValue = Object.entries(JSON.parse(_grantNode[2]))[0][1]
const parent_path = needGrantNodes
.slice(i + 1)
.map((item) => {
return Number(item.split('%')[0])
})
.reverse()
.join(',')
for (let j = 0; j < floor; j++) {
const itemList = needGrantRids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) =>
grantCiType(typeId, rid, {
id_filter: { [ciId]: { name: uniqueValue, parent_path } },
is_recursive: Number(i > 0),
})
)
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
}
callback(result)
},
clickCheckbox(treeKey) {
const _idx = this.batchTreeKey.findIndex((item) => item === treeKey)
if (_idx > -1) {
this.batchTreeKey.splice(_idx, 1)
} else {
this.batchTreeKey.push(treeKey)
}
},
batchDeleteCIRelationFromTree() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete')}</div>,
async onOk() {
for (let i = 0; i < that.batchTreeKey.length; i++) {
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
that.batchTreeKey[i],
'delete'
)
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
await deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {})
}
that.$message.success(that.$t('deleteSuccess'))
that.showBatchLevel = null
that.batchTreeKey = []
setTimeout(() => {
that.reload()
}, 500)
},
})
},
async handleSingleRevoke({ users = [], roles = [] }, treeKey, callback) {
const rids = [...users.map((item) => Number(item.split('-')[1])), ...roles]
const treeKeyPath = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = treeKeyPath.pop(-1).split('%')
const id_filter = {}
const typeId = _treeKey[1]
const ciId = _treeKey[0]
const uniqueValue = Object.entries(JSON.parse(_treeKey[2]))[0][1]
const parent_path = treeKeyPath
.map((item) => {
return Number(item.split('%')[0])
})
.join(',')
id_filter[ciId] = { name: uniqueValue, parent_path }
const floor = Math.ceil(rids.length / 6)
const result = []
for (let j = 0; j < floor; j++) {
const itemList = rids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) => revokeCiType(typeId, rid, { id_filter, perms: ['read'], parent_path }))
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
callback(result)
},
async handleRevoke({ users = [], roles = [] }) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
const treeKey = this.batchTreeKey[i]
await this.handleSingleRevoke({ users, roles }, treeKey, (_result) => {
result.push(..._result)
})
}
} else {
await this.handleSingleRevoke({ users, roles }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
this.showBatchLevel = null
this.batchTreeKey = []
},
}, },
} }
</script> </script>

View File

@ -11,24 +11,24 @@
> >
<div :style="{ width: '100%' }" id="add-table-modal"> <div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<!-- <a-input
v-model="expression"
class="ci-searchform-expression"
:style="{ width, marginBottom: '10px' }"
:placeholder="placeholder"
@focus="
() => {
isFocusExpression = true
}
"
/> -->
<SearchForm <SearchForm
ref="searchForm" ref="searchForm"
:typeId="addTypeId" :typeId="addTypeId"
:preferenceAttrList="preferenceAttrList" :preferenceAttrList="preferenceAttrList"
@refresh="handleSearch" @refresh="handleSearch"
/> >
<!-- <a @click="handleSearch"><a-icon type="search"/></a> --> <a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table <vxe-table
ref="xTable" ref="xTable"
row-id="_id" row-id="_id"
@ -77,19 +77,31 @@
/> />
</a-spin> </a-spin>
</div> </div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal> </a-modal>
</template> </template>
<script> <script>
/* eslint-disable no-useless-escape */
import { searchCI } from '@/modules/cmdb/api/ci' import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference' import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation' import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper' import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue' import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default { export default {
name: 'AddTableModal', name: 'AddTableModal',
components: { SearchForm }, components: { SearchForm, CreateInstanceForm },
data() { data() {
return { return {
visible: false, visible: false,
@ -106,6 +118,7 @@ export default {
type: 'children', type: 'children',
preferenceAttrList: [], preferenceAttrList: [],
ancestor_ids: undefined, ancestor_ids: undefined,
attrList1: [],
} }
}, },
computed: { computed: {
@ -119,6 +132,13 @@ export default {
return this.isFocusExpression ? '500px' : '100px' return this.isFocusExpression ? '500px' : '100px'
}, },
}, },
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {}, watch: {},
methods: { methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) { async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
@ -132,6 +152,9 @@ export default {
await getSubscribeAttributes(addTypeId).then((res) => { await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列 this.preferenceAttrList = res.attributes // 已经订阅的全部列
}) })
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true) this.getTableData(true)
}, },
async getTableData(isInit) { async getTableData(isInit) {
@ -207,6 +230,9 @@ export default {
this.handleClose() this.handleClose()
this.$emit('reload') this.$emit('reload')
}, 500) }, 500)
} else {
this.handleClose()
this.$emit('reload')
} }
}, },
handleSearch() { handleSearch() {

View File

@ -1,63 +1,81 @@
<template> <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 <div
:style="{ :class="{
width: '100%', 'relation-views-node': true,
display: 'inline-flex', 'relation-views-node-checkbox': showCheckbox,
justifyContent: 'space-between',
alignItems: 'center',
}" }"
@click="clickNode" @click="clickNode"
> >
<span <span>
:style="{ <a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
display: 'flex',
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<template v-if="icon"> <template v-if="icon">
<img <img
v-if="icon.split('$$')[2]" v-if="icon.includes('$$') && icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`" :src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }" :style="{ maxHeight: '14px', maxWidth: '14px' }"
/> />
<ops-icon <ops-icon
v-else v-else-if="icon.includes('$$') && icon.split('$$')[0]"
:style="{ :style="{
color: icon.split('$$')[1], color: icon.split('$$')[1],
fontSize: '14px', fontSize: '14px',
}" }"
:type="icon.split('$$')[0]" :type="icon.split('$$')[0]"
/> />
<span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
</template> </template>
<span <span class="relation-views-node-title">{{ this.title }}</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> </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> <a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div> </div>
</a-dropdown>
</template> </template>
<script> <script>
@ -88,7 +106,15 @@ export default {
type: Boolean, type: Boolean,
default: () => false, default: () => false,
}, },
ciTypes: { ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
@ -141,14 +167,10 @@ export default {
icon() { icon() {
const _split = this.treeKey.split('@^@') const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
return _find?.icon || null
}, },
ciTypeName() { showCheckbox() {
const _split = this.treeKey.split('@^@') return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.name || ''
}, },
}, },
methods: { methods: {
@ -159,8 +181,73 @@ export default {
this.$emit('onNodeClick', this.treeKey) this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down' this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
}, },
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
}, },
} }
</script> </script>
<style></style> <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>

View File

@ -284,6 +284,10 @@ export default {
const regSort = /(?<=sort=).+/g const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
// if (exp) {
// exp = exp.replace(/(\:)/g, '$1*')
// exp = exp.replace(/(\,)/g, '*$1')
// }
// 如果是表格点击的排序 以表格为准 // 如果是表格点击的排序 以表格为准
let sort let sort
if (sortByTable) { if (sortByTable) {
@ -314,7 +318,9 @@ export default {
this.columnsGroup = [] this.columnsGroup = []
this.instanceList = [] this.instanceList = []
this.totalNumber = res['numfound'] this.totalNumber = res['numfound']
if (!res['numfound']) {
return
}
const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({ const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({
type_ids: Object.keys(res.counter).join(','), type_ids: Object.keys(res.counter).join(','),
}) })

View File

@ -10,11 +10,12 @@
:noOptionsText="$t('cs.components.empty')" :noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'" :class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:limit="20" :limit="limit"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
v-bind="$attrs" v-bind="$attrs"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:flat="flat"
> >
</treeselect> </treeselect>
</template> </template>
@ -60,6 +61,14 @@ export default {
type: String, type: String,
default: 'employee_id', default: 'employee_id',
}, },
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return {} return {}