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>
</div>
@ -42,6 +43,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
@ -80,6 +82,7 @@
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
@ -103,6 +106,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
@ -125,6 +129,7 @@
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
@ -133,6 +138,7 @@
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' }">
@ -155,6 +161,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<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: ';' }) : ''"
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>
@ -177,8 +186,9 @@
<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">
<div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
@ -211,6 +221,10 @@ export default {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {

View File

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

View File

@ -113,6 +113,10 @@ export default {
},
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'

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

@ -14,9 +14,16 @@
<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,
@ -25,6 +32,9 @@ export default {
},
computed: {
title() {
if (this.customTitle) {
return this.customTitle
}
if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser')
}

View File

@ -6,7 +6,8 @@
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
v-model="radioValue"
:value="radioValue"
@change="changeRadioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
@ -128,6 +129,9 @@ export default {
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')
@ -149,10 +153,6 @@ export default {
})
}
}
} else {
this.form = {
name: '',
}
}
},
async handleOk() {
@ -198,6 +198,13 @@ export default {
}
this.expression = expression
},
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
},
}
</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' }"
@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">
{{ $t('cmdb.components.ciSearchTips') }}
</template>
@ -97,6 +97,7 @@
</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
@ -191,6 +192,9 @@ export default {
}
},
methods: {
// toggleAdvanced() {
// this.advanced = !this.advanced
// },
getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res

View File

@ -46,8 +46,9 @@ const cmdb_en = {
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies',
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}]?',
@ -223,7 +224,7 @@ const cmdb_en = {
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',
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',
@ -466,7 +467,7 @@ const cmdb_en = {
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',
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',
@ -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!',
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!',

View File

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

View File

@ -220,6 +220,7 @@ export default {
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
@ -296,6 +297,38 @@ export default {
_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
@ -363,6 +396,9 @@ export default {
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)

View File

@ -27,7 +27,7 @@
</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' }">
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>

View File

@ -270,7 +270,17 @@
</div>
</el-select>
</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
size="small"
filterable

View File

@ -2,14 +2,55 @@
<div :style="{ marginBottom: '-24px', overflow: 'hidden' }">
<div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper">
<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>
</div>
<SplitPane
:min="200"
:max="500"
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-relation-views"
:appName="`cmdb-relation-views-${viewId}`"
triggerColor="#F0F5FF"
:triggerLength="18"
>
@ -24,7 +65,6 @@
@drop="onDrop"
:expandedKeys="expandedKeys"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu
:title="title"
@ -35,7 +75,10 @@
:id2type="relationViews.id2type"
@onContextMenuClick="onContextMenuClick"
@onNodeClick="onNodeClick"
:ciTypes="ciTypes"
:ciTypeIcons="ciTypeIcons"
:showBatchLevel="showBatchLevel"
:batchTreeKey="batchTreeKey"
@clickCheckbox="clickCheckbox"
/>
</template>
</a-tree>
@ -313,9 +356,8 @@
v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert>
<AddTableModal ref="addTableModal" @reload="reload" />
<!-- <GrantDrawer ref="grantDrawer" resourceTypeName="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])" />
<create-instance-form
ref="create"
@ -325,11 +367,12 @@
/>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ReadPermissionsModal ref="readPermissionsModal" />
<RevokeModal ref="revokeModal" @handleRevoke="handleRevoke" />
</div>
</template>
<script>
/* eslint-disable no-useless-escape */
import _ from 'lodash'
import { Tree } from 'element-ui'
import Sortable from 'sortablejs'
@ -349,7 +392,7 @@ import {
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
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 { searchResourceType } from '@/modules/acl/api/resource'
import SplitPane from '@/components/SplitPane'
@ -361,8 +404,11 @@ import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant'
import GrantModal from '../../components/cmdbGrant/grantModal.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
export default {
name: 'RelationViews',
@ -370,8 +416,8 @@ export default {
SearchForm,
AddTableModal,
ContextMenu,
// GrantDrawer,
CMDBGrant,
GrantModal,
SplitPane,
ElTree: Tree,
EditAttrsPopover,
@ -382,13 +428,15 @@ export default {
PasswordField,
PreferenceSearch,
OpsMoveIcon,
ReadPermissionsModal,
RevokeModal,
},
data() {
return {
treeData: [],
triggerSelect: false,
treeNode: null,
ciTypes: [],
ciTypeIcons: {},
relationViews: {},
levels: [],
showTypeIds: [],
@ -430,6 +478,10 @@ export default {
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
contextMenuKey: null,
showBatchLevel: null,
batchTreeKey: [],
}
},
@ -452,6 +504,21 @@ export default {
isShowBatchIcon() {
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() {
return {
@ -505,8 +572,8 @@ export default {
})
},
getCITypesList() {
getCITypes().then((res) => {
this.ciTypes = res.ci_types
getCITypeIcons().then((res) => {
this.ciTypeIcons = res
})
},
refreshTable() {
@ -572,33 +639,38 @@ export default {
q = q.slice(1)
}
if (this.treeKeys.length === 0) {
await this.judgeCITypes(q)
// await this.judgeCITypes(q)
if (!refreshType) {
this.loadRoot()
await this.loadRoot()
}
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
} else {
q = `q=_type:${this.currentTypeId[0]},` + q
}
if (this.currentTypeId[0]) {
const res = await searchCI2(q)
this.pageNo = res.page
this.numfound = res.numfound
res.result.forEach((item, index) => (item.key = item._id))
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
console.log(jsonAttrList)
this.instanceList = res['result'].map((item) => {
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
return { ..._.cloneDeep(item) }
})
this.initialInstanceList = _.cloneDeep(this.instanceList)
this.calcColumns()
}
// const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
// if (fuzzySearch) {
// q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
// } else {
// q = `q=_type:${this.currentTypeId[0]},` + q
// }
// if (this.currentTypeId[0] && this.treeData && this.treeData.length) {
// // default select first node
// this.onNodeClick(this.treeData[0].key)
// const res = await searchCI2(q)
// const root_id = this.treeData.map((item) => item.id).join(',')
// q += `&root_id=${root_id}`
// this.pageNo = res.page
// this.numfound = res.numfound
// res.result.forEach((item, index) => (item.key = item._id))
// const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
// console.log(jsonAttrList)
// this.instanceList = res['result'].map((item) => {
// jsonAttrList.forEach(
// (jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
// )
// return { ..._.cloneDeep(item) }
// })
// this.initialInstanceList = _.cloneDeep(this.instanceList)
// this.calcColumns()
// }
} else {
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}`
@ -634,10 +706,10 @@ export default {
level = [1]
}
q += `&level=${level.join(',')}`
await this.judgeCITypes(q)
if (!refreshType) {
this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level)
}
await this.judgeCITypes(q)
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
@ -645,8 +717,12 @@ export default {
q = `q=_type:${this.currentTypeId[0]},` + q
}
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]) {
const res = await searchCIRelation(q)
@ -666,7 +742,6 @@ export default {
this.calcColumns()
}
if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => {
let ancestor_ids
@ -684,8 +759,9 @@ export default {
ancestor_ids,
root_ids: key.split('%')[0],
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')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((res) => {
let result
const getTreeItem = (data, id) => {
@ -741,22 +817,25 @@ export default {
const promises = _showTypeIds.map((typeId) => {
let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
if (Object.values(this.level2constraint).includes('2')) {
_q = _q + `&&has_m2m=1`
_q = _q + `&has_m2m=1`
}
console.log(_q)
if (this.treeKeys.length === 0) {
return searchCI2(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
if (this.root_parent_path) {
_q = _q + `&root_parent_path=${this.root_parent_path}`
}
})
} else {
// if (this.treeKeys.length === 0) {
// 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) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
}
})
}
// }
})
await Promise.all(promises).then(async () => {
if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) {
@ -780,7 +859,7 @@ export default {
},
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 ciIds = []
res.result.forEach((item) => {
@ -797,8 +876,9 @@ export default {
return statisticsCIRelation({
root_ids: ciIds.join(','),
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')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@ -806,16 +886,17 @@ export default {
})
})
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) {
const rootId = rootIdAndTypeId.split('%')[0]
const typeId = Number(rootIdAndTypeId.split('%')[1])
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
const index = topo_flatten.findIndex((id) => id === typeId)
const _type = topo_flatten[index + 1]
const index = this.topo_flatten.findIndex((id) => id === typeId)
const _type = this.topo_flatten[index + 1]
if (_type) {
let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000`
if (
@ -829,8 +910,12 @@ export default {
.join(',')}`
}
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) => {
const facet = []
const ciIds = []
@ -852,8 +937,9 @@ export default {
ancestor_ids,
root_ids: ciIds.join(','),
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')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@ -862,7 +948,7 @@ export default {
}
})
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.treeNode = treeNode
// this.refreshTable()
resolve()
})
},
@ -979,8 +1066,7 @@ export default {
this.$refs.xTable.refreshColumn()
})
},
onContextMenuClick(treeKey, menuKey) {
if (treeKey) {
calculateParamsFromTreeKey(treeKey, menuKey) {
const splitTreeKey = treeKey.split('@^@')
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%')
const firstCIObj = JSON.parse(_tempTree[2])
@ -996,10 +1082,21 @@ export default {
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
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') {
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
const that = this
this.$confirm({
title: that.$t('warning'),
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 {
const childTypeId = menuKey
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
@ -1066,8 +1181,10 @@ export default {
const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '')
if (_splitDragKey.length - 1 === _splitTargetKey.length) {
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]
console.log(_splitDragKey)
// TODO 拖拽这里不造咋弄 等等再说吧
batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => {
this.reload()
})
@ -1438,6 +1555,138 @@ export default {
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>

View File

@ -11,24 +11,24 @@
>
<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> -->
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
@ -77,19 +77,31 @@
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</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'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'AddTableModal',
components: { SearchForm },
components: { SearchForm, CreateInstanceForm },
data() {
return {
visible: false,
@ -106,6 +118,7 @@ export default {
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
@ -119,6 +132,13 @@ export default {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
@ -132,6 +152,9 @@ export default {
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
@ -207,6 +230,9 @@ export default {
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {

View File

@ -1,63 +1,81 @@
<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',
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span
:style="{
display: 'flex',
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<span>
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
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
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
: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 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>
</a-dropdown>
</template>
<script>
@ -88,7 +106,15 @@ export default {
type: Boolean,
default: () => false,
},
ciTypes: {
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
@ -141,14 +167,10 @@ export default {
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
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? 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 || ''
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
},
methods: {
@ -159,8 +181,73 @@ export default {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
},
}
</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 exp = expression.match(regQ) ? expression.match(regQ)[0] : null
// if (exp) {
// exp = exp.replace(/(\:)/g, '$1*')
// exp = exp.replace(/(\,)/g, '*$1')
// }
// 如果是表格点击的排序 以表格为准
let sort
if (sortByTable) {
@ -314,7 +318,9 @@ export default {
this.columnsGroup = []
this.instanceList = []
this.totalNumber = res['numfound']
if (!res['numfound']) {
return
}
const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({
type_ids: Object.keys(res.counter).join(','),
})

View File

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