feat(ui): update resource view menu display and CMDB route redirection

This commit is contained in:
LH_R
2025-08-15 16:04:10 +08:00
parent f7273c96dc
commit 035171cbe8
12 changed files with 1207 additions and 915 deletions

View File

@@ -0,0 +1,60 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip,
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis',
},
tooltip: {
type: Boolean,
},
length: {
type: Number,
required: true,
},
lines: {
type: Number,
default: 1,
},
fullWidthRecognition: {
type: Boolean,
default: false,
},
},
methods: {
getStrDom(str, fullLength) {
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
},
getTooltip(fullStr, fullLength) {
return (
<Tooltip overlayStyle={{ maxWidth: '700px' }}>
<template slot="title">{fullStr}</template>
{this.getStrDom(fullStr, fullLength)}
</Tooltip>
)
},
},
render() {
const { tooltip, length } = this.$props
const str = this.$slots.default.map((vNode) => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return strDom
},
}
</script>

View File

@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

View File

@@ -1,14 +1,6 @@
import router, { resetRouter } from '@/router'
import Menu from 'ant-design-vue/es/menu' import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon' import Icon from 'ant-design-vue/es/icon'
import store from '@/store'
import {
subscribeCIType,
subscribeTreeView,
} from '@/modules/cmdb/api/preference'
import { searchResourceType } from '@/modules/acl/api/resource' import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import styles from './index.module.less' import styles from './index.module.less'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
@@ -87,40 +79,6 @@ export default {
inject: ['reload'], inject: ['reload'],
methods: { methods: {
...mapActions(['UpdateCMDBSEarchValue']), ...mapActions(['UpdateCMDBSEarchValue']),
cancelAttributes(e, menu) {
const that = this
e.preventDefault()
e.stopPropagation()
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
onOk() {
const citypeId = menu.meta.typeId
const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree]).then(() => {
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(citypeId) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
const href = window.location.href
const hrefSplit = href.split('/')
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
that.$router.push('/cmdb/preference')
}
const roles = store.getters.roles
resetRouter()
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.appRoutes)
})
if (hrefSplit[hrefSplit.length - 1] === 'preference') {
that.reload()
}
})
},
})
},
// select menu item // select menu item
onOpenChange(openKeys) { onOpenChange(openKeys) {
if (this.mode === 'horizontal') { if (this.mode === 'horizontal') {
@@ -170,7 +128,6 @@ export default {
return this.$t(`${title}`) return this.$t(`${title}`)
}, },
renderMenuItem(menu) { renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const target = menu.meta.target || null const target = menu.meta.target || null
const tag = target && 'a' || 'router-link' const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } } const props = { to: { name: menu.name } }
@@ -187,26 +144,11 @@ export default {
<tag {...{ props, attrs }}> <tag {...{ props, attrs }}>
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })} {this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
<span> <span>
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span> <span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>
{isShowDot && !menu.meta.disabled && {this.renderI18n(menu.meta.title)}
<a-popover </span>
overlayClassName="custom-menu-extra-submenu"
placement="rightTop"
arrowPointAtCenter
autoAdjustOverflow={false}
getPopupContainer={(trigger) => trigger}
content={() =>
<div>
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />{ this.renderI18n('grant') }</div>
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />{ this.renderI18n('cmdb.preference.cancelSub') }</div>
</div>}
>
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
</a-popover>
}
</span> </span>
</tag> </tag>
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
</Item> </Item>
) )
}, },
@@ -269,27 +211,6 @@ export default {
) )
} }
}, },
handlePerm(e, menu, resource_type_name) {
e.stopPropagation()
e.preventDefault()
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name,
perm: 'grant',
resource_name: menu.meta.name,
}).then(res => {
if (res.result) {
console.log(menu)
if (resource_type_name === 'CIType') {
this.$refs.cmdbGrantCIType.open({ name: menu.meta.name, cmdbGrantType: 'ci', CITypeId: menu.meta?.typeId })
} else {
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
}
} else {
this.$message.error(this.$t('noPermission'))
}
})
},
jumpCMDBSearch(value) { jumpCMDBSearch(value) {
this.UpdateCMDBSEarchValue(value) this.UpdateCMDBSEarchValue(value)

View File

@@ -2,10 +2,12 @@ import MultiTab from '@/components/MultiTab'
import Result from '@/components/Result' import Result from '@/components/Result'
import TagSelect from '@/components/TagSelect' import TagSelect from '@/components/TagSelect'
import ExceptionPage from '@/components/Exception' import ExceptionPage from '@/components/Exception'
import Ellipsis from '@/components/Ellipsis'
export { export {
MultiTab, MultiTab,
Result, Result,
ExceptionPage, ExceptionPage,
TagSelect TagSelect,
Ellipsis
} }

View File

@@ -38,7 +38,7 @@
<script> <script>
import store from '@/store' import store from '@/store'
import { gridSvg, top_agent, top_acl } from '@/core/icons' import { gridSvg, top_agent, top_acl } from '@/core/icons'
import { getPreference } from '@/modules/cmdb/api/preference'
export default { export default {
name: 'TopMenu', name: 'TopMenu',
components: { gridSvg, top_agent, top_acl }, components: { gridSvg, top_agent, top_acl },
@@ -77,19 +77,8 @@ export default {
async handleClick(route) { async handleClick(route) {
this.visible = false this.visible = false
if (route.name !== this.current) { if (route.name !== this.current) {
if (route.name === 'cmdb') {
const preference = await getPreference()
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.type_ids.some((item) => item === Number(lastTypeId))) {
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
} else {
this.$router.push('/cmdb/dashboard')
}
} else {
this.$router.push(route.redirect) this.$router.push(route.redirect)
} }
// this.current = route.name
}
}, },
}, },
} }

View File

@@ -2,3 +2,8 @@ export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid) const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : '' return idx > -1 ? 'background-color:#E0E7FF!important' : ''
} }
export const getCurrentRowClass = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'grant-table-row-focus' : ''
}

View File

@@ -81,8 +81,6 @@
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import router, { resetRouter } from '@/router'
import store from '@/store'
import { import {
subscribeCIType, subscribeCIType,
getSubscribeAttributes, getSubscribeAttributes,
@@ -223,9 +221,8 @@ export default {
selectedAttrList.map((item) => { selectedAttrList.map((item) => {
return [item, !!this.fixedList.includes(item)] return [item, !!this.fixedList.includes(item)]
}) })
).then((res) => { ).then(() => {
this.$message.success(this.$t('cmdb.components.subSuccess')) this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute()
if (this.selectedAttrList.length > 0) { if (this.selectedAttrList.length > 0) {
this.instanceSubscribed = true this.instanceSubscribed = true
} else { } else {
@@ -233,13 +230,7 @@ export default {
} }
}) })
}, },
resetRoute() {
resetRouter()
const roles = store.getters.roles
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.appRoutes)
})
},
setTargetKeys(targetKeys) { setTargetKeys(targetKeys) {
this.selectedAttrList = targetKeys this.selectedAttrList = targetKeys
}, },

View File

@@ -1,5 +1,5 @@
import { RouteView, BasicLayout } from '@/layouts' import { RouteView, BasicLayout } from '@/layouts'
import { getPreference, getRelationView } from '@/modules/cmdb/api/preference' import { getRelationView } from '@/modules/cmdb/api/preference'
const genCmdbRoutes = async () => { const genCmdbRoutes = async () => {
const routes = { const routes = {
@@ -7,6 +7,7 @@ const genCmdbRoutes = async () => {
name: 'cmdb', name: 'cmdb',
component: BasicLayout, component: BasicLayout,
meta: { title: 'CMDB', keepAlive: false }, meta: { title: 'CMDB', keepAlive: false },
redirect: '/cmdb/instances/types',
children: [ children: [
// preference // preference
// views // views
@@ -39,12 +40,10 @@ const genCmdbRoutes = async () => {
}, },
}, },
{ {
path: '/cmdb/resourceviews', path: '/cmdb/instances/types/:typeId?',
name: 'cmdb_resource_views', name: 'cmdb_resource_views',
component: RouteView, component: () => import(`../views/ci/index`),
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: true }, meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: false }
hideChildrenInMenu: false,
children: []
}, },
{ {
path: '/cmdb/tree_views', path: '/cmdb/tree_views',
@@ -176,35 +175,8 @@ const genCmdbRoutes = async () => {
} }
] ]
} }
// Dynamically add subscription items and business relationships // get service tree dynamic display menu
const [preference, relation] = await Promise.all([getPreference(), getRelationView()]) const relation = await getRelationView()
const resourceViewsIndex = routes.children.findIndex(item => item.name === 'cmdb_resource_views')
preference.group_types.forEach(group => {
if (preference.group_types.length > 1) {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/group${group.id}`,
name: `cmdb_instances_group_${group.id}`,
meta: { title: group.name || 'other', disabled: true, style: 'margin-left: 12px' },
})
}
group.ci_types.forEach(item => {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/${item.id}`,
component: () => import(`../views/ci/index`),
name: `cmdb_${item.id}`,
meta: { title: item.alias, keepAlive: false, typeId: item.id, name: item.name, customIcon: item.icon },
// hideChildrenInMenu: true // Force display of MenuItem instead of SubMenu
})
})
})
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.type_ids.some(item => item === Number(lastTypeId))) {
routes.redirect = `/cmdb/instances/types/${lastTypeId}`
} else if (routes.children[resourceViewsIndex]?.children?.length > 0) {
routes.redirect = routes.children[resourceViewsIndex].children.find(item => !item.hidden && !item.meta.disabled)?.path
} else {
routes.redirect = '/cmdb/dashboard'
}
if (relation?.name2id?.length === 0) { if (relation?.name2id?.length === 0) {
const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views') const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,829 @@
<template>
<div id="ciIndex" class="cmdb-ci">
<a-spin :tip="loadTip" :spinning="loading" >
<div class="cmdb-views-header">
<span>
<span class="cmdb-views-header-title">{{ CIType.alias || CIType.name }}</span>
<span
@click="
() => {
$refs.metadataDrawer.open(typeId)
}
"
class="cmdb-views-header-metadata"
>
<a-icon type="info-circle" />{{ $t('cmdb.ci.attributeDesc') }}
</span>
</span>
<a-space>
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="$refs.create.handleOpen(true, 'create')"
><ops-icon type="veops-increase" />
{{ $t('create') }}
</a-button>
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs">
<a-button
type="primary"
ghost
class="ops-button-ghost"
><ops-icon type="veops-configuration_table" />{{ $t('cmdb.configTable') }}</a-button
>
</EditAttrsPopover>
<a-dropdown v-model="visible">
<a-button type="primary" ghost class="ops-button-ghost">···</a-button>
<a-menu slot="overlay" @click="handleMenuClick">
<a-menu-item @click="handlePerm" key="grant">
<a-icon type="user-add" />
{{ $t('grant') }}
</a-menu-item>
<a-menu-item
v-if="!autoSub.enabled"
key="cancelSub"
@click="unsubscribe"
>
<a-icon type="star" />
{{ $t('cmdb.preference.cancelSub') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space>
</div>
<div class="cmdb-ci-main">
<SearchForm
ref="search"
@refresh="handleSearch"
:preferenceAttrList="preferenceAttrList"
:typeId="typeId"
:selectedRowKeys="selectedRowKeys"
@copyExpression="copyExpression"
>
<PreferenceSearch
ref="preferenceSearch"
v-show="!selectedRowKeys.length"
@getQAndSort="getQAndSort"
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<a-divider type="vertical" />
<span @click="batchRollback">{{ $t('cmdb.ci.rollback') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
<CiDetailDrawer ref="detail" :typeId="typeId" />
<CITable
ref="xTable"
:id="`cmdb-ci-${typeId}`"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
:height="tableHeight"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
:current="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@showSizeChange="onShowSizeChange"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="
(page) => {
currentPage = page
}
"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
<create-instance-form
ref="create"
:typeIdFromProp="typeId"
@reload="reloadData"
@submit="batchUpdate"
/>
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<CiRollbackForm ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
<MetadataDrawer ref="metadataDrawer" />
<CMDBGrant ref="cmdbGrant" resourceTypeName="CIType" app_id="cmdb" />
</div>
</a-spin>
</div>
</template>
<script>
import _ from 'lodash'
import Sortable from 'sortablejs'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById, getAttrPassword } from '@/modules/cmdb/api/CITypeAttr'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
import { CIBaselineRollback } from '@/modules/cmdb/api/history'
import { getCITableColumns } from '../../utils/helper'
import { intersection } from '@/utils/functions/set'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from './modules/MetadataDrawer.vue'
import CMDBGrant from '../../components/cmdbGrant'
import CiRollbackForm from './modules/ciRollbackForm.vue'
import SearchForm from '@/modules/cmdb/components/searchForm/SearchForm.vue'
import CreateInstanceForm from './modules/CreateInstanceForm'
import CiDetailDrawer from './modules/ciDetailDrawer.vue'
import EditAttrsPopover from './modules/editAttrsPopover'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'InstanceList',
components: {
SearchForm,
CreateInstanceForm,
CiDetailDrawer,
EditAttrsPopover,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
CMDBGrant,
CiRollbackForm,
CITable
},
props: {
typeId: {
type: Number,
default: undefined
},
CIType: {
type: Object,
default: () => {}
},
autoSub: {
type: Object,
default: () => {}
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
// if (this.selectedRowKeys && this.selectedRowKeys.length) {
// return this.windowHeight - 246
// }
return this.windowHeight - 240
},
},
data() {
return {
tableData: [],
loading: false,
currentPage: 1,
pageSizeOptions: ['50', '100', '200', '100000'],
pageSize: 50,
totalNumber: 0,
loadTip: '',
form: this.$form.createForm(this),
preferenceAttrList: [],
instanceList: [],
columns: [],
// custom table alert & rowSelection
selectedRowKeys: [],
// Check whether to edit
initialInstanceList: [],
sortByTable: undefined,
isEditActive: false,
attrList: [],
attributes: {},
// Table drag parameters
tableDragClassName: [],
resource_type: {},
initialPasswordValue: {},
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
visible: false,
}
},
watch: {
currentPage: function(newVal, oldVal) {
this.loadTableData(this.sortByTable)
},
},
provide() {
return {
handleSearch: this.handleSearch,
setPreferenceSearchCurrent: this.setPreferenceSearchCurrent,
attrList: () => {
return this.attrList
},
attributes: () => {
return this.attributes
},
filterCompPreferenceSearch: () => {
return { type_id: this.typeId }
},
resource_type: () => {
return this.resource_type
}
}
},
async mounted() {
this.loading = true
await this.getAttributeList()
await this.loadPreferenceAttrList()
await this.loadTableData()
this.loading = false
this.$nextTick(() => {
const loadingNode = document.getElementsByClassName('ant-drawer-mask')
if (loadingNode?.style) {
loadingNode.style.zIndex = 8
}
})
setTimeout(() => {
this.columnDrop()
}, 1000)
},
beforeDestroy() {
// window.onkeypress = null
if (this.sortable) {
this.sortable.destroy()
}
},
methods: {
async getAttributeList() {
await getCITypeAttributesById(this.typeId).then((res) => {
this.attrList = res.attributes
this.attributes = res
})
},
handleSearch() {
this.$refs.xTable.getVxetableRef().clearSort()
this.sortByTable = undefined
this.$nextTick(() => {
if (this.currentPage === 1) {
this.reloadData()
} else {
this.currentPage = 1
}
})
},
async loadTableData(sortByTable = undefined) {
try {
this.loading = true
// If fuzzy search is possible, queryParam can be deleted later.
// const queryParams = this.$refs['search'].queryParam || {}
const fuzzySearch = this.$refs['search'].fuzzySearch
const expression = this.$refs['search'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
let sort
if (sortByTable) {
sort = sortByTable
} else {
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
}
const res = await searchCI({
q: `_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: this.pageSize,
page: this.currentPage,
sort,
})
this.totalNumber = res['numfound']
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
const jsonAttrList = this.attrList.filter((attr) => attr.value_type === '6')
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.$nextTick(() => {
// this.setSelectRows()
this.$refs.xTable.getVxetableRef().refreshColumn()
})
} finally {
this.loading = false
}
},
getColumns(data, attrList) {
const width = document.getElementById('ciIndex').clientWidth - 50
return getCITableColumns(data, attrList, width)
},
setSelectRows() {
const cached = new Set(this.selectedRowKeys)
const loaded = new Set(this.instanceList.map((i) => i.ci_id || i._id))
const inter = Array.from(intersection(cached, loaded))
if (inter.length === this.instanceList.length) {
this.$refs['xTable'].getVxetableRef().setAllCheckboxRow(true)
} else {
const rows = []
inter.forEach((rid) => {
rows.push(this.$refs['xTable'].getVxetableRef().getRowById(rid))
})
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
}
},
async loadPreferenceAttrList() {
const subscribed = await getSubscribeAttributes(this.typeId)
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
},
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
reloadData() {
this.loadTableData()
},
handleEditClose({ row, rowIndex, column }) {
if (!this.isContinueCloseEdit) {
return
}
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (
!(item.field in this.initialPasswordValue) &&
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
) {
data[item.field] = row[item.field] ?? null
}
})
Object.keys(this.initialPasswordValue).forEach((key) => {
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
data[key] = this.passwordValue[key]
row[key] = this.passwordValue[key]
}
})
this.isEditActive = false
this.lastEditCiId = null
if (JSON.stringify(data) !== '{}') {
updateCI(row.ci_id || row._id, data)
.then(() => {
this.$message.success(this.$t('saveSuccess'))
$table.reloadRow(row, null)
const _initialInstanceList = _.cloneDeep(this.initialInstanceList)
_initialInstanceList[rowIndex] = {
..._initialInstanceList[rowIndex],
...data,
}
this.initialInstanceList = _initialInstanceList
})
.catch((err) => {
console.log(err)
this.loadTableData()
})
}
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
},
async openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.CIType.alias || this.CIType.name,
})
},
batchDownload({ filename, type, checkedKeys }) {
const jsonAttrList = []
checkedKeys.forEach((key) => {
const _find = this.attrList.find((attr) => attr.name === key)
if (_find && _find.value_type === '6') jsonAttrList.push(key)
})
const data = _.cloneDeep([
...this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords(),
...this.$refs.xTable.getVxetableRef().getCheckboxRecords(true),
])
this.$refs.xTable.getVxetableRef().exportData({
filename,
type,
columnFilterMethod({ column }) {
return checkedKeys.includes(column.property)
},
data: [
...data.map((item) => {
jsonAttrList.forEach((jsonAttr) => (item[jsonAttr] = item[jsonAttr] ? JSON.stringify(item[jsonAttr]) : ''))
return { ...item }
}),
],
})
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
batchUpdate(values) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.ci.batchUpdateConfirm'),
async onOk() {
that.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {}
Object.keys(values).forEach((key) => {
// Field values support blanking
// There are currently field values that do not support blanking and will be returned by the backend.
if (values[key] === undefined || values[key] === null) {
payload[key] = null
} else {
payload[key] = values[key]
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
})
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.reloadData()
},
batchDelete() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
that.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchDeleting')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
deleteCI(record) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
deleteCI(record.ci_id || record._id).then((res) => {
// that.$refs.table.refresh(true)
that.$message.success(that.$t('deleteSuccess'))
that.reloadData()
})
},
})
},
batchRollback() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen(true)
})
},
async batchRollbackAsync(params) {
const mask = document.querySelector('.ant-drawer-mask')
const oldValue = mask.style.zIndex
mask.style.zIndex = 2
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.rollbackingTips')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => CIBaselineRollback(x, params))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchRollbacking', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
mask.style.zIndex = oldValue
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
async refreshAfterEditAttrs() {
await this.loadPreferenceAttrList()
await this.loadTableData()
},
onShowSizeChange(current, pageSize) {
this.pageSize = pageSize
if (this.currentPage === 1) {
this.reloadData()
} else {
this.currentPage = 1
}
setTimeout(() => {
// this.setSelectRows()
}, 500)
},
handleSortCol({ column, property, order, sortBy, sortList, $event }) {
let sortByTable
if (order === 'asc') {
sortByTable = property
} else if (order === 'desc') {
sortByTable = `-${property}`
}
this.sortByTable = sortByTable
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData(sortByTable)
} else {
this.currentPage = 1
}
})
},
columnDrop() {
this.$nextTick(() => {
const xTable = this.$refs.xTable.getVxetableRef()
this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{
handle: '.vxe-handle',
onChoose: () => {
const header = xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row')
const classNameList = []
header.childNodes.forEach((item) => {
classNameList.push(item.classList[1])
})
this.tableDragClassName = classNameList
},
onEnd: (params) => {
// 由于开启了虚拟滚动newIndex和oldIndex是虚拟的
const { newIndex, oldIndex } = params
// 从tableDragClassName拿到colid
const fromColid = this.tableDragClassName[oldIndex]
const toColid = this.tableDragClassName[newIndex]
const fromColumn = xTable.getColumnById(fromColid)
const toColumn = xTable.getColumnById(toColid)
const fromIndex = xTable.getColumnIndex(fromColumn)
const toIndex = xTable.getColumnIndex(toColumn)
const tableColumn = xTable.getColumns()
const currRow = tableColumn.splice(fromIndex, 1)[0]
tableColumn.splice(toIndex, 0, currRow)
xTable.loadColumn(tableColumn)
},
}
)
})
},
handleEditActived() {
this.isEditActive = true
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
const { row, column } = editRecord
if (passwordCol.length && this.lastEditCiId !== row._id) {
this.$nextTick(async () => {
for (let i = 0; i < passwordCol.length; i++) {
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
this.initialPasswordValue[passwordCol[i].field] = res.value
this.passwordValue[passwordCol[i].field] = res.value
})
}
this.isContinueCloseEdit = false
await this.$refs.xTable.getVxetableRef().clearEdit()
this.isContinueCloseEdit = true
this.$nextTick(() => {
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
})
})
}
this.lastEditCiId = row._id
})
},
getQAndSort() {
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
const expression = this.$refs['search'].expression || ''
this.$refs.preferenceSearch.savePreference({ fuzzySearch, expression })
},
setParamsFromPreferenceSearch(item) {
const { fuzzySearch, expression } = item.option
this.$refs.search.fuzzySearch = fuzzySearch
this.$refs.search.expression = expression
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$refs.xTable.getVxetableRef().clearSort()
this.sortByTable = undefined
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
setPreferenceSearchCurrent(id = null) {
this.$refs.preferenceSearch.currentPreferenceSearch = id
},
copyExpression() {
const expression = this.$refs['search'].expression || ''
const fuzzySearch = this.$refs['search'].fuzzySearch
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const text = `q=_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
unsubscribe() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', {
name: `${this.CIType.alias || this.CIType.name}`,
}),
onOk: () => {
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')]
Promise.all(promises).then(() => {
this.$message.success(this.$t('cmdb.preference.cancelSubSuccess'))
this.$emit('unSubscribe')
})
},
})
},
handlePerm() {
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name: 'CIType',
perm: 'grant',
resource_name: this.CIType.name,
}).then((res) => {
if (res.result) {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
this.$nextTick(() => {
this.$refs.cmdbGrant.open({
name: this.CIType.name,
cmdbGrantType: 'ci',
CITypeId: this.typeId,
})
})
})
} else {
this.$message.error(this.$t('noPermission'))
}
})
},
handleMenuClick(e) {
if (e.key === 'grant') {
this.visible = false
}
},
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
</script>
<style lang="less">
@import '../index.less';
</style>
<style lang="less" scoped>
.cmdb-ci {
background-color: #fff;
padding: 20px;
border-radius: @border-radius-box;
height: calc(100vh - 64px);
overflow: auto;
margin-bottom: -24px;
}
</style>

View File

@@ -191,6 +191,10 @@ export default {
CIReferenceAttr CIReferenceAttr
}, },
props: { props: {
typeIdFromProp: {
type: Number,
default: 0,
},
typeIdFromRelation: { typeIdFromRelation: {
type: Number, type: Number,
default: 0, default: 0,
@@ -235,7 +239,7 @@ export default {
if (this.typeIdFromRelation) { if (this.typeIdFromRelation) {
return this.typeIdFromRelation return this.typeIdFromRelation
} }
return this.$router.currentRoute.meta.typeId return this.typeIdFromProp
}, },
valueTypeMap() { valueTypeMap() {
return valueTypeMap() return valueTypeMap()