feat(ui): add automatic subscription #285

This commit is contained in:
LH_R
2025-08-15 16:06:23 +08:00
parent 035171cbe8
commit 0144ee6508
5 changed files with 347 additions and 54 deletions

View File

@@ -157,3 +157,18 @@ export function preferenceCitypeOrder(data) {
data: data data: data
}) })
} }
export function getAutoSubscription() {
return axios({
url: '/v0.1/preference/auto_subscription',
method: 'get',
})
}
export function putAutoSubscription(data) {
return axios({
url: '/v0.1/preference/auto_subscription',
method: 'put',
data
})
}

View File

@@ -222,12 +222,14 @@ const cmdb_en = {
otherGroupTips: 'Non sortable within the other group', otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}', filterTips: 'click to show {name}',
attributeAssociation: 'Attribute Association', attributeAssociation: 'Attribute Association',
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models', attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, long text, boolean, reference) of two models',
attributeAssociationTip2: 'Double click to edit', attributeAssociationTip2: 'Double click to edit',
attributeAssociationTip3: 'Two Attributes must be selected', attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType', attributeAssociationTip4: 'Please select a attribute from Source CIType',
attributeAssociationTip5: 'Please select a attribute from Target CIType', attributeAssociationTip5: 'Please select a attribute from Target CIType',
attributeAssociationTip6: 'Cannot be deleted again.', attributeAssociationTip6: 'Cannot be deleted again.',
attributeAssociationTip7: '1. The attribute value types of the source model and target model must be consistent.',
attributeAssociationTip8: '2. One To Many: Source model can select multiple value attributes',
show: 'show attribute', show: 'show attribute',
setAsShow: 'Set as show attribute', setAsShow: 'Set as show attribute',
cancelSetAsShow: 'Cancel show attribute', cancelSetAsShow: 'Cancel show attribute',
@@ -460,6 +462,20 @@ const cmdb_en = {
searchPlaceholder: 'Please search CIType', searchPlaceholder: 'Please search CIType',
subCITable: 'Data', subCITable: 'Data',
subCITree: 'Tree', subCITree: 'Tree',
autoSub: 'Auto Subscription',
autoSub2: 'Click To Enable Auto Subscribe',
autoSubScope: 'Auto Subscription Scope',
subscribeAllModel: 'Subscribe All Models',
selectiveSubscription: 'Selective Subscription',
excludeGroup: 'Exclude Group',
excludeModel: 'Exclude Model',
selectGroup: 'Select Group',
selectModel: 'Select Model',
isEnable: 'Is Enable',
enableAutoSubTip: 'After enabling automatic subscription, the model list in the resource data menu will only be based on the results of automatic subscription.',
tips1: 'Please go to',
tips2: ' Preference ',
tips3: 'page first to complete your subscription!'
}, },
custom_dashboard: { custom_dashboard: {
charts: 'Chart', charts: 'Chart',
@@ -748,7 +764,6 @@ if __name__ == "__main__":
searchTips: 'Search in service tree' searchTips: 'Search in service tree'
}, },
tree: { tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings', subSettings: 'Settings',
}, },
topo: { topo: {

View File

@@ -222,12 +222,14 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序', otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性', filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联', attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系', attributeAssociationTip1: '通过2个模型的属性值(除密码、json、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑', attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性', attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性', attributeAssociationTip4: '请选择原模型属性',
attributeAssociationTip5: '请选择目标模型属性', attributeAssociationTip5: '请选择目标模型属性',
attributeAssociationTip6: '不可再删除', attributeAssociationTip6: '不可再删除',
attributeAssociationTip7: '1. 源模型和目标模型的属性值类型必须保持一致',
attributeAssociationTip8: '2. 一对多:源模型可选多值属性',
show: '展示属性', show: '展示属性',
setAsShow: '设置为展示属性', setAsShow: '设置为展示属性',
cancelSetAsShow: '取消设置为展示属性', cancelSetAsShow: '取消设置为展示属性',
@@ -459,6 +461,20 @@ const cmdb_zh = {
searchPlaceholder: '请搜索模型', searchPlaceholder: '请搜索模型',
subCITable: '数据订阅', subCITable: '数据订阅',
subCITree: '层级订阅', subCITree: '层级订阅',
autoSub: '自动订阅',
autoSub2: '点击开启自动订阅',
autoSubScope: '自动订阅范围',
subscribeAllModel: '订阅所有模型',
selectiveSubscription: '选择性订阅',
excludeGroup: '排除分组',
excludeModel: '排除模型',
selectGroup: '选择分组',
selectModel: '选择模型',
isEnable: '是否启用',
enableAutoSubTip: '开启自动订阅后,资源数据菜单的模型列表只以自动订阅的结果为准',
tips1: '请先到',
tips2: ' 我的订阅 ',
tips3: '页面完成订阅!',
}, },
custom_dashboard: { custom_dashboard: {
charts: '图表', charts: '图表',
@@ -747,7 +763,6 @@ if __name__ == "__main__":
searchTips: '在服务树中筛选' searchTips: '在服务树中筛选'
}, },
tree: { tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置', subSettings: '订阅设置',
}, },
topo: { topo: {

View File

@@ -0,0 +1,177 @@
<template>
<a-modal
:title="$t('cmdb.preference.autoSub')"
:visible="visible"
:width="600"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="autuSubFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 7 }"
:wrapper-col="{ span: 15 }"
>
<a-form-model-item
:label="$t('cmdb.preference.autoSubScope')"
prop="base_strategy"
>
<a-radio-group
v-model="form.base_strategy"
:options="baseStrategyOptions"
/>
</a-form-model-item>
<a-form-model-item
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeGroup') : $t('cmdb.preference.selectGroup')"
prop="group_ids"
>
<a-select
v-model="form.group_ids"
mode="multiple"
optionFilterProp="title"
:options="groupSelectOptions"
/>
</a-form-model-item>
<a-form-model-item
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeModel') : $t('cmdb.preference.selectModel')"
prop="type_ids"
>
<a-select
v-model="form.type_ids"
mode="multiple"
optionFilterProp="title"
>
<a-select-opt-group
v-for="(group) in modelSelectOptions"
:key="group.value"
:title="group.label"
>
<span slot="label">{{ group.label }}</span>
<a-select-option
v-for="(type) in group.children"
:key="type.value"
:value="type.value"
:title="type.label"
>
{{ type.label }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.preference.isEnable')"
prop="enabled"
:extra="$t('cmdb.preference.enableAutoSubTip')"
>
<a-switch v-model="form.enabled" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { putAutoSubscription } from '@/modules/cmdb/api/preference.js'
export default {
name: 'AutoSubscribe',
props: {
ciType: {
type: Array,
default: () => []
},
autoSub: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
form: {
base_strategy: 'all',
group_ids: [],
type_ids: [],
enabled: true,
},
rules: {
base_strategy: [{ required: true, message: this.$t('placeholder2') }],
},
baseStrategyOptions: [
{
label: this.$t('cmdb.preference.subscribeAllModel'),
value: 'all'
},
{
label: this.$t('cmdb.preference.selectiveSubscription'),
value: 'none'
}
],
groupSelectOptions: [],
modelSelectOptions: []
}
},
methods: {
async open() {
this.form = {
base_strategy: this.autoSub?.base_strategy || 'all',
group_ids: this.autoSub?.group_ids || [],
type_ids: this.autoSub?.type_ids || [],
enabled: this.autoSub?.enabled ?? true
}
this.groupSelectOptions = this.ciType.map((group) => {
return {
label: group.name,
title: group.name,
value: group.id
}
})
const modelSelectOptions = this.ciType.filter((group) => group?.ci_types?.length)
this.modelSelectOptions = modelSelectOptions.map((group) => {
return {
label: group.name,
value: group.id,
children: group.ci_types.map((type) => {
return {
label: type.alias || type.name,
value: type.id
}
})
}
})
this.visible = true
},
handleCancel() {
this.visible = false
},
handleOk() {
this.$refs.autuSubFormRef.validate(async (valid) => {
if (valid) {
const { base_strategy, group_ids, type_ids, enabled } = this.form
const params = {
base_strategy: base_strategy,
group_ids: group_ids.join(','),
type_ids: type_ids.join(','),
enabled: enabled
}
putAutoSubscription(params).then(() => {
this.$message.success(this.$t('saveSuccess'))
this.handleCancel()
this.$emit('ok')
})
}
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -86,6 +86,7 @@
</div> </div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span> <span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action"> <span class="cmdb-preference-group-content-action">
<template v-if="!enableAutoSub || subType.type === 'tree'">
<a-tooltip :title="$t('cmdb.preference.cancelSub')"> <a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span <span
@click="unsubscribe(ciType, group.type)" @click="unsubscribe(ciType, group.type)"
@@ -93,6 +94,7 @@
</span> </span>
</a-tooltip> </a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" /> <a-divider type="vertical" :style="{ margin: '0 3px' }" />
</template>
<a-tooltip :title="$t('cmdb.preference.editSub')"> <a-tooltip :title="$t('cmdb.preference.editSub')">
<span <span
@click="openSubscribeSetting(ciType, `${index + 1}`)" @click="openSubscribeSetting(ciType, `${index + 1}`)"
@@ -108,11 +110,23 @@
</div> </div>
</div> </div>
<div class="cmdb-preference-right"> <div class="cmdb-preference-right">
<div class="cmdb-preference-right-header">
<a-input-search <a-input-search
v-model="searchValue" v-model="searchValue"
:style="{ width: '300px', marginBottom: '20px' }" class="cmdb-preference-right-header-search"
:placeholder="$t('cmdb.preference.searchPlaceholder')" :placeholder="$t('cmdb.preference.searchPlaceholder')"
/> />
<div
:class="[
'cmdb-preference-right-header-auto',
enableAutoSub ? 'cmdb-preference-right-header-auto_enable' : ''
]"
@click="openAutoSubModal"
>
<ops-icon type="auto" />
<span>{{ enableAutoSub ? $t('cmdb.preference.autoSub') : $t('cmdb.preference.autoSub2') }}</span>
</div>
</div>
<div v-for="group in filterCiTypeData" :key="group.id"> <div v-for="group in filterCiTypeData" :key="group.id">
<p <p
@click="changeGroupExpand(group)" @click="changeGroupExpand(group)"
@@ -154,14 +168,6 @@
{{ item.alias || item.name }}</span {{ item.alias || item.name }}</span
> >
</div> </div>
<div class="cmdb-preference-colleague">
<span
v-if="type_id2users[item.id] && type_id2users[item.id].length"
>{{ type_id2users[item.id].length > 99 ? '99+' : type_id2users[item.id].length
}}{{ $t('cmdb.preference.peopleSub') }}</span
>
<span v-else>{{ $t('cmdb.preference.noSub') }}</span>
</div>
<div class="cmdb-preference-progress"> <div class="cmdb-preference-progress">
<div class="cmdb-preference-progress-info"> <div class="cmdb-preference-progress-info">
<span>{{ $t('cmdb.menu.ad') }}</span> <span>{{ $t('cmdb.menu.ad') }}</span>
@@ -173,18 +179,28 @@
</div> </div>
<a-divider :style="{ margin: '10px 0 3px 0' }" /> <a-divider :style="{ margin: '10px 0 3px 0' }" />
<div class="cmdb-preference-footor-subscribed" v-if="item.is_subscribed"> <div class="cmdb-preference-footor-subscribed" v-if="item.is_subscribed">
<span><a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}</span> <span
:style="{
opacity: enableAutoSub ? 0 : 1
}"
>
<a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}
</span>
<span> <span>
<template v-if="!enableAutoSub">
<a-tooltip :title="$t('cmdb.preference.cancelSub')"> <a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span> <span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span>
</a-tooltip> </a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" /> <a-divider type="vertical" :style="{ margin: '0 3px' }" />
</template>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')"> <a-tooltip :title="$t('cmdb.preference.editSub')">
<span @click="openSubscribeSetting(item)"><ops-icon type="cmdb-preference-subscribe"/></span> <span @click="openSubscribeSetting(item)"><ops-icon type="cmdb-preference-subscribe"/></span>
</a-tooltip> </a-tooltip>
</span> </span>
</div> </div>
<div v-else class="cmdb-preference-footor-unsubscribed"> <div v-else class="cmdb-preference-footor-unsubscribed">
<template v-if="!enableAutoSub">
<a <a
@click="handleSubscribeCIType(item)" @click="handleSubscribeCIType(item)"
class="cmdb-preference-footor-unsubscribed-item" class="cmdb-preference-footor-unsubscribed-item"
@@ -192,6 +208,7 @@
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }} <ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
</a> </a>
<span class="cmdb-preference-footor-unsubscribed-gap"></span> <span class="cmdb-preference-footor-unsubscribed-gap"></span>
</template>
<a <a
@click="openSubscribeSetting(item, '2')" @click="openSubscribeSetting(item, '2')"
class="cmdb-preference-footor-unsubscribed-item" class="cmdb-preference-footor-unsubscribed-item"
@@ -209,17 +226,21 @@
ref="subscribeSetting" ref="subscribeSetting"
@reload=" @reload="
() => { () => {
resetRoute() initData()
} }
" "
/> />
<AutoSubscribe
ref="autoSubRef"
:ciType="citypeData"
:autoSub="autoSub"
@ok="initData"
/>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import router, { resetRouter } from '@/router'
import store from '@/store'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import moment from 'moment' import moment from 'moment'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
@@ -238,10 +259,12 @@ import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting
import { getCIAdcStatistics } from '../../api/ci' import { getCIAdcStatistics } from '../../api/ci'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons' import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '../ipam/constants.js' import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '../ipam/constants.js'
import AutoSubscribe from './components/autoSubscribe.vue'
import { getAutoSubscription } from '@/modules/cmdb/api/preference.js'
export default { export default {
name: 'Preference', name: 'Preference',
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis }, components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis, AutoSubscribe },
data() { data() {
return { return {
citypeData: [], citypeData: [],
@@ -253,6 +276,7 @@ export default {
type_id2users: {}, type_id2users: {},
myPreferences: [], myPreferences: [],
searchValue: '', searchValue: '',
autoSub: {}
} }
}, },
computed: { computed: {
@@ -275,11 +299,19 @@ export default {
} }
return this.citypeData return this.citypeData
}, },
enableAutoSub() {
return this?.autoSub?.enabled ?? false
}
}, },
mounted() { mounted() {
this.getCITypes(true) this.getCITypes(true)
this.getAutoSubscription()
}, },
methods: { methods: {
initData() {
this.getCITypes()
this.getAutoSubscription()
},
async getCITypes(isInit = false) { async getCITypes(isInit = false) {
const [ciTypeGroup, pref, pref2, statistics] = await Promise.all([ const [ciTypeGroup, pref, pref2, statistics] = await Promise.all([
getCITypeGroups({ need_other: true }), getCITypeGroups({ need_other: true }),
@@ -350,6 +382,12 @@ export default {
}, 300) }, 300)
} }
}, },
async getAutoSubscription() {
const res = await getAutoSubscription()
this.autoSub = res || {}
},
getsubscribedDays(item) { getsubscribedDays(item) {
const subscribedTime = this.self.type_id2subs_time[item.id] const subscribedTime = this.self.type_id2subs_time[item.id]
moment.duration(moment().diff(moment(subscribedTime))) moment.duration(moment().diff(moment(subscribedTime)))
@@ -396,21 +434,11 @@ export default {
} }
} }
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess')) that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
that.resetRoute() that.initData()
}) })
}, },
}) })
}, },
resetRoute() {
const roles = store.getters.roles
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
resetRouter()
this.$nextTick(() => {
router.addRoutes(store.getters.appRoutes)
this.getCITypes()
})
})
},
async handleSubscribeCIType(ciType) { async handleSubscribeCIType(ciType) {
try { try {
@@ -433,7 +461,7 @@ export default {
subscribeList subscribeList
) )
this.$message.success(this.$t('cmdb.components.subSuccess')) this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute() this.initData()
} catch (error) { } catch (error) {
console.error('handleSubscribeCIType failed', error) console.error('handleSubscribeCIType failed', error)
this.$message.success(this.$t('cmdb.components.subFailed')) this.$message.success(this.$t('cmdb.components.subFailed'))
@@ -461,7 +489,7 @@ export default {
}) })
preferenceCitypeOrder({ type_ids: typeIds, is_tree: false }) preferenceCitypeOrder({ type_ids: typeIds, is_tree: false })
.then(() => { .then(() => {
this.resetRoute() this.initData()
}) })
.catch(() => { .catch(() => {
this.getCITypes(false) this.getCITypes(false)
@@ -487,13 +515,17 @@ export default {
preferenceCitypeOrder({ type_ids: typeIds, is_tree: isTree }) preferenceCitypeOrder({ type_ids: typeIds, is_tree: isTree })
.then(() => { .then(() => {
if (!isTree) { if (!isTree) {
this.resetRoute() this.initData()
} }
}) })
.catch(() => { .catch(() => {
this.getCITypes(false) this.getCITypes(false)
}) })
}, },
openAutoSubModal() {
this.$refs.autoSubRef.open()
}
}, },
} }
</script> </script>
@@ -629,6 +661,45 @@ export default {
height: 100%; height: 100%;
padding-top: 24px; padding-top: 24px;
&-header {
margin-bottom: 20px;
display: flex;
align-items: center;
&-search {
width: 300px;
margin-right: 14px;
}
&-auto {
background: linear-gradient(90deg, #16D9E3 0%, #30C7EC 47%, #46AEF7 100%);
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: #FFFFFF;
cursor: pointer;
padding: 0 12px;
opacity: 0.5;
transition: opacity 0.2s;
span {
margin-left: 4px;
font-size: 14px;
font-weight: 600;
}
&_enable {
opacity: 1;
}
&:hover {
opacity: 1;
}
}
}
&-group-title { &-group-title {
width: 300px; width: 300px;
margin-bottom: 20px; margin-bottom: 20px;
@@ -651,7 +722,7 @@ export default {
.cmdb-preference-type { .cmdb-preference-type {
display: inline-block; display: inline-block;
width: 195px; width: 195px;
height: 155px; height: 127px;
border-radius: @border-radius-box; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
box-shadow: ~'0px 2px 8px @{primary-color}15'; box-shadow: ~'0px 2px 8px @{primary-color}15';