diff --git a/dbm-ui/frontend/.eslintignore b/dbm-ui/frontend/.eslintignore index 541a2a38d7..55042961bd 100644 --- a/dbm-ui/frontend/.eslintignore +++ b/dbm-ui/frontend/.eslintignore @@ -2,3 +2,4 @@ node_modules/ dist/ public/ src/types/auto-imports.d.ts +patch/ diff --git a/dbm-ui/frontend/package.json b/dbm-ui/frontend/package.json index ff381b9dbc..b3ed0da68d 100644 --- a/dbm-ui/frontend/package.json +++ b/dbm-ui/frontend/package.json @@ -18,6 +18,7 @@ "@blueking/bk-weweb": "0.0.15", "@blueking/bkflow.js": "0.1.10", "@blueking/ip-selector": "0.0.1-beta.126", + "@icon-cool/bk-icon-bk-biz-components": "0.0.4", "@vueuse/core": "10.2.1", "axios": "1.2.1", "bkui-vue": "0.0.2-beta.68", diff --git a/dbm-ui/frontend/patch/user-selector/alternate-item.tsx b/dbm-ui/frontend/patch/user-selector/alternate-item.tsx new file mode 100644 index 0000000000..ebeafc51ef --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/alternate-item.tsx @@ -0,0 +1,120 @@ +// @ts-nocheck +import { computed, defineComponent, toRefs, withModifiers } from 'vue'; + +import RenderAvatar from './render-avatar'; +import RenderList from './render-list'; +import tooltips from './tooltips'; + +export default defineComponent({ + name: 'AlternateItem', + directives: { + tooltips, + }, + // props: ['selector', 'user', 'keyword', 'index'], + props: { + selector: { + type: Object, + }, + user: { + type: Object, + }, + keyword: { + type: String, + }, + index: { + type: Number, + }, + }, + setup(props) { + const { selector, user, keyword } = toRefs(props); + const disabled = computed(() => selector.value.disabledUsers.includes(user.value.username)); + const getItemContent = () => { + const [nameWithoutDomain, domain] = user.value.username.split('@'); + let displayText = nameWithoutDomain; + if (keyword.value) { + displayText = displayText.replace( + new RegExp(keyword.value, 'g'), + `${keyword.value}`, + ); + } + const displayUsername = selector.value.displayDomain && domain + ? `${displayText}@${domain}` + : displayText; + + const displayName = user.value.display_name; + if (displayName) { + return `${displayUsername}(${displayName})`; + } + + return displayUsername; + }; + const getTitle = () => selector.value.getDisplayText(user.value); + + return { + disabled, + getItemContent, + getTitle, + }; + }, + render() { + return ( +
  • e.stopPropagation()} + onMousedown={withModifiers(() => this.selector.handleUserMousedown(this.user, this.disabled), ['left', 'stop'])} + onMouseup={withModifiers(() => this.selector.handleUserMouseup(this.user, this.disabled), ['left', 'stop'])}> + { + this.selector.renderList + ? <> + + + + : <> + { + this.selector.tagType === 'avatar' + ? <> + + + + : null + } + { + this.selector.displayListTips && this.user.category_name + ? <> + + { this.user.category_name } + + + : null + } + + + } +
  • + ); + }, +}); diff --git a/dbm-ui/frontend/patch/user-selector/alternate-list.tsx b/dbm-ui/frontend/patch/user-selector/alternate-list.tsx new file mode 100644 index 0000000000..fdb08056ed --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/alternate-list.tsx @@ -0,0 +1,165 @@ +/* eslint-disable no-prototype-builtins */ + +import { hideAll } from 'tippy.js'; +import { + type ComponentPublicInstance, + computed, + defineComponent, + getCurrentInstance, + type HTMLAttributes, + nextTick, + ref, + watch, + withModifiers, +} from 'vue'; + +import AlternateItem from './alternate-item'; +import instanceStore from './instance-store'; + +export default defineComponent({ + setup() { + const { proxy } = getCurrentInstance(); + instanceStore.setInstance('alternateContent', 'alternateList', proxy); + + const selector = ref(null); + const keyword = ref(''); + const next = ref(true); + const loading = ref(true); + const matchedUsers = ref([]); + const wrapperStyle = computed(() => { + const style: any = {}; + if (selector.value?.panelWidth) { + style.width = `${parseInt(selector.value.panelWidth, 10)}px`; + } + return style; + }); + const listStyle = computed(() => { + const style = { + 'max-height': '192px', + }; + if (selector.value) { + const maxHeight = parseInt(selector.value.listScrollHeight, 10); + if (!isNaN(maxHeight)) { + style['max-height'] = `${maxHeight}px`; + } + } + return style; + }); + const getIndex = (index: number, childIndex = 0) => { + let flattenedIndex = 0; + matchedUsers.value.slice(0, index).forEach((user) => { + if (user.hasOwnProperty('children')) { + flattenedIndex += user.children.length; + } else { + flattenedIndex += 1; + } + }); + return flattenedIndex + childIndex; + }; + + const handleScroll = () => { + hideAll({ exclude: selector.value.inputRef, duration: 0 }); + if (loading.value || !next.value) { + return false; + } + const list = alternateList.value; + const threshold = 32; + if (list.scrollTop + list.clientHeight > list.scrollHeight - threshold) { + selector.value.search(keyword.value, next.value); + } + }; + + watch(keyword, () => { + alternateItem.value = []; + nextTick(() => { + alternateList.value.scrollTop = 0; + }); + }); + + const alternateListContainer = ref(null); + const alternateList = ref(null); + const alternateItem = ref([]); + + const setRef = (el: HTMLElement | ComponentPublicInstance | HTMLAttributes) => { + alternateItem.value.push(el); + }; + + return { + selector, + keyword, + next, + loading, + matchedUsers, + wrapperStyle, + listStyle, + getIndex, + handleScroll, + alternateListContainer, + alternateList, + alternateItem, + setRef, + }; + }, + render() { + return ( +
    + + { + (!this.loading && !this.matchedUsers.length) + ? <> +

    + { this.selector.emptyText } +

    + + : null + } +
    + ); + }, +}); diff --git a/dbm-ui/frontend/patch/user-selector/icon-user.svg b/dbm-ui/frontend/patch/user-selector/icon-user.svg new file mode 100644 index 0000000000..4bdf4f533d --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/icon-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dbm-ui/frontend/patch/user-selector/index.ts b/dbm-ui/frontend/patch/user-selector/index.ts new file mode 100644 index 0000000000..250dbc4e80 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/index.ts @@ -0,0 +1,31 @@ +import type { App, Plugin } from 'vue'; + +import request from './request'; +import UserSelector from './selector.vue'; + +// UserSelector.install = (Vue) => { +// window.$vueApp.component(UserSelector.name, UserSelector); +// }; + +// export default UserSelector; + +// export { request }; + +export interface OriginComponent { + name: string; + install?: Plugin; +} + +const withInstall = (component: T): T & Plugin => { + // eslint-disable-next-line no-param-reassign + component.install = function (app: App, { prefix } = {}) { + const pre = app.config.globalProperties.bkUIPrefix || prefix || 'Bk'; + app.component(pre + component.name, component); + }; + return component as T & Plugin; +}; + +export { request }; + +const BkUserSelector = withInstall(UserSelector); +export default BkUserSelector; diff --git a/dbm-ui/frontend/patch/user-selector/instance-store.ts b/dbm-ui/frontend/patch/user-selector/instance-store.ts new file mode 100644 index 0000000000..fedc963480 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/instance-store.ts @@ -0,0 +1,16 @@ +import { type ComponentPublicInstance } from 'vue'; + +const store = {}; + +export default { + setInstance(group: string, id: string, proxy: ComponentPublicInstance) { + if (!store[group]) { + store[group] = {}; + } + + store[group][id] = proxy; + }, + getInstance(group: string, id: string) { + return store[group][id]; + }, +}; diff --git a/dbm-ui/frontend/patch/user-selector/render-alternate.ts b/dbm-ui/frontend/patch/user-selector/render-alternate.ts new file mode 100644 index 0000000000..3d7cc39a91 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/render-alternate.ts @@ -0,0 +1,20 @@ +// import * as Vue from 'vue'; +// export default { +// name: 'render-alternate', +// data() { +// return { selector: null }; +// }, +// render() { +// return this.selector.defaultAlternate(Vue.h); +// }, +// }; + +import { h } from 'vue'; + +export default { + name: 'render-alternate', + props: ['selector'], + setup(props: any): any { + return () => props.selector.defaultAlternate(h); + }, +}; diff --git a/dbm-ui/frontend/patch/user-selector/render-avatar.tsx b/dbm-ui/frontend/patch/user-selector/render-avatar.tsx new file mode 100644 index 0000000000..886945aaa1 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/render-avatar.tsx @@ -0,0 +1,51 @@ +// export default { +// name: 'render-avatar', +// props: ['user', 'urlMethod'], +// render() { +// return ; +// }, +// async created() { +// try { +// let avatar = null; +// if (typeof this.user === 'string') { +// avatar = await this.urlMethod(this.user); +// } else if (typeof this.user === 'object') { +// avatar = this.user.avatar +// || this.user.logo +// || (await this.urlMethod(this.user.username)); +// } +// if (avatar) { +// this.$el.style.backgroundImage = `url(${avatar})`; +// } +// } catch (e) {} +// }, +// }; + +import { onMounted, ref, watch } from 'vue'; + +export default { + name: 'render-avatar', + props: ['user', 'urlMethod'], + setup(props: any) { + const avatar = ref(''); + onMounted(async () => { + try { + if (typeof props.user === 'string') { + avatar.value = await props.urlMethod(props.user); + } else if (typeof props.user === 'object') { + avatar.value = props.user.avatar || props.user.logo || (await props.urlMethod(props.user.username)); + } + } catch (e) {} + }); + + const userSelectorAvatarRef = ref(null); + + watch(avatar, (v: string) => { + if (v) { + userSelectorAvatarRef.value.style.backgroundImage = `url(${avatar.value})`; + } + }); + + return () => ; + }, +}; diff --git a/dbm-ui/frontend/patch/user-selector/render-list.ts b/dbm-ui/frontend/patch/user-selector/render-list.ts new file mode 100644 index 0000000000..2712e201f8 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/render-list.ts @@ -0,0 +1,43 @@ +import { h } from 'vue'; + +// export default { +// name: 'render-list', +// props: ['selector', 'user', 'index', 'keyword', 'disabled'], +// render() { +// return this.selector.renderList(h, { +// user: this.user, +// index: this.index, +// keyword: this.keyword, +// disabled: this.disabled, +// }); +// }, +// }; + +export default { + name: 'render-list', + props: { + selector: { + type: Object, + }, + user: { + type: Object, + }, + keyword: { + type: String, + }, + index: { + type: Number, + }, + disabled: { + type: Boolean, + }, + }, + setup(props: any): any { + return () => props.selector.renderList(h, { + user: props.user, + index: props.index, + keyword: props.keyword, + disabled: props.disabled, + }); + }, +}; diff --git a/dbm-ui/frontend/patch/user-selector/render-tag.ts b/dbm-ui/frontend/patch/user-selector/render-tag.ts new file mode 100644 index 0000000000..113f18216d --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/render-tag.ts @@ -0,0 +1,44 @@ +// import * as Vue from 'vue'; +// export default { +// name: 'render-tag', +// props: ['username', 'user', 'index'], +// render() { +// return this.$parent.renderTag(Vue.h, { +// username: this.username, +// index: this.index, +// user: this.user, +// }); +// }, +// }; + +import { h, inject } from 'vue'; + +export default { + name: 'render-tag', + props: ['username', 'user', 'index'], + // props: { + // selector: { + // type: Object, + // }, + // user: { + // type: Object, + // }, + // keyword: { + // type: String, + // }, + // index: { + // type: Number, + // }, + // disabled: { + // type: Boolean, + // }, + // }, + setup(props: any) { + const parentSelector = inject('parentSelector'); + return () => (parentSelector as any).renderTag(h, { + username: props.username, + index: props.index, + user: props.user, + }); + }, +}; diff --git a/dbm-ui/frontend/patch/user-selector/request.ts b/dbm-ui/frontend/patch/user-selector/request.ts new file mode 100644 index 0000000000..7fea1455fc --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/request.ts @@ -0,0 +1,222 @@ +// @ts-nocheck + +import { createApp, getCurrentInstance, ref, watch } from 'vue'; + +import instanceStore from './instance-store'; + +let callbackSeed = 0; +function JSONP(api: string, params = {}, options: any = {}) { + return new Promise((resolve, reject) => { + let timer: number; + const callbackName = `USER_LIST_CALLBACK_${callbackSeed += 1}`; + window[callbackName] = (response: any) => { + timer && clearTimeout(timer); + document.body.removeChild(script); + delete window[callbackName]; + resolve(response); + }; + const script = document.createElement('script'); + script.onerror = (_event) => { + document.body.removeChild(script); + delete window[callbackName]; + reject('Get user list failed.'); + }; + const query = []; + // eslint-disable-next-line no-restricted-syntax + for (const key in params) { + query.push(`${key}=${params[key]}`); + } + script.src = `${api}?${query.join('&')}&callback=${callbackName}`; + if (options.timeout) { + setTimeout(() => { + document.body.removeChild(script); + delete window[callbackName]; + reject('Get user list timeout.'); + }, options.timeout); + } + document.body.appendChild(script); + }); +} + +// 缓存已经加载过的人员 +// 以api为key,存储不同数据源的用户 +const userMap = new Map(); + +function getMap(api: string) { + if (userMap.has(api)) { + return userMap.get(api); + } + const map = new Map(); + userMap.set(api, map); + return map; +} + +function storeUsers(api: string, users: any) { + const map = getMap(api); + users.forEach((user: any) => map.set(user.username, user)); +} + +function getUsers(api: string, usernames: any) { + const map = getMap(api); + const users: string[] = []; + usernames.forEach((username: string) => { + if (map.has(username)) { + users.push(map.get(username)); + } + }); + return users; +} + +// 接口最大支持100条记录,超过100条需要将请求拆分为多个 +async function handleBatchSearch(api: string, usernames: string[], options: any) { + const map = getMap(api); + const unique = [...new Set(usernames)].filter(username => !map.has(username)); + if (!unique.length) { + return Promise.resolve(getUsers(api, usernames)); + } + const slices: string[][] = []; + unique.reduce((slice, username, index) => { + if (slice.length < 100) { + slice.push(username); + if (index === unique.length - 1) { + slices.push(slice); + } + return slice; + } + slices.push(slice); + return []; + }, []); + try { + const responses = await Promise.all(slices.map(slice => JSONP( + api, + { + app_code: 'bk-magicbox', + exact_lookups: slice.join(','), + page_size: 100, + page: 1, + }, + options, + ))); + responses.forEach((response: any) => { + if (response.code !== 0) return; + storeUsers(api, response.data.results || []); + }); + } catch (error) { + console.error(error); + } + return Promise.resolve(getUsers(api, usernames)); +} + +function createVm(apiStr: string) { + const app = { + setup() { + const { proxy } = getCurrentInstance(); + instanceStore.setInstance('exactSearch', apiStr, proxy); + + const api = ref(''); + api.value = apiStr; + + const queue = ref([]); + + watch(() => queue, (q: any) => { + q.value.length && dispatchSeach(); + }, { deep: true }); + + const search = usernames => new Promise((resolve) => { + queue.value.push({ + resolve, + usernames, + }); + }); + + const dispatchSeach = async () => { + const currentQueue = [...queue.value]; + queue.value = []; + try { + const allNames = currentQueue.reduce( + (all, { usernames }) => all.concat(usernames), + [], + ); + const users = await request.exactSearch(api.value, allNames); + const map = {}; + users.forEach((user) => { + map[user.username] = user; + }); + currentQueue.forEach(({ resolve, usernames }) => { + const resolveData = []; + usernames.forEach((username) => { + // eslint-disable-next-line no-prototype-builtins + if (map.hasOwnProperty(username)) { + resolveData.push(map[username]); + } + }); + resolve(resolveData); + }); + } catch (error) { + currentQueue.forEach(({ resolve }) => { + resolve([]); + }); + console.error(error); + } + }; + + return { + api, + queue, + search, + dispatchSeach, + }; + }, + }; + + const vm = createApp(app); + const vmContainer = document.createElement('div'); + vm.mount(vmContainer); + // document.body.appendChild(vmContainer); + // vmMap.set(apiStr, vm); + return vm; +} + +const request = { + // 模糊搜索,失败时返回空 + async fuzzySearch(api, params, options) { + const data = {}; + try { + const response = await JSONP(api, params, options); + if (response.code !== 0) { + throw new Error(response); + } + data.count = response.data.count; + data.results = response.data.results || []; + storeUsers(api, data.results); + } catch (error) { + console.error(error.message); + data.count = 0; + data.results = []; + } + return data; + }, + // 精确搜索,不在此处捕获异常,在上层捕获并显示在tooltips中 + async exactSearch(api, username, options) { + const isArray = Array.isArray(username); + const usernames = isArray ? username : [username]; + const users = await handleBatchSearch(api, usernames, options); + if (isArray) { + return users; + } + return users[0]; + }, + // 粘贴时对粘贴的用户进行校验 + pasteValidate(api, usernames, options) { + return handleBatchSearch(api, usernames, options); + }, + // 队列式查询,用于多个组件共存时,批量拉取已存在的用户信息 + scheduleExactSearch(api, username) { + const usernames = Array.isArray(username) ? username : [username]; + createVm(api); + const vm = instanceStore.getInstance('exactSearch', api); + return vm.search(usernames); + }, +}; + +export default request; diff --git a/dbm-ui/frontend/patch/user-selector/selector.vue b/dbm-ui/frontend/patch/user-selector/selector.vue new file mode 100644 index 0000000000..6a2ff9f538 --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/selector.vue @@ -0,0 +1,1198 @@ + + + + + diff --git a/dbm-ui/frontend/patch/user-selector/style.css b/dbm-ui/frontend/patch/user-selector/style.css new file mode 100644 index 0000000000..947e99f1cc --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/style.css @@ -0,0 +1,398 @@ +.user-selector * { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +.user-selector { + display: inline-block; + min-width: 120px; + font-size: 14px; + color: #63656e; + cursor: text; +} + +.user-selector.user-selector-info { + display: inline; + min-width: initial; + color: inherit; +} + +.user-selector .user-selector-layout { + position: relative; + height: 100%; +} + +.user-selector .user-selector-layout .user-selector-container { + position: relative; + min-width: 100%; + min-height: 100%; + padding: 0 9px 0 3px; + overflow: hidden; + font-size: 0; + line-height: 1; + background-color: #fff; + border: 1px solid #c4c6cc; + border-radius: 2px; +} + +.user-selector .user-selector-layout .user-selector-container.is-flex-height { + min-height: 32px; +} + +.user-selector .user-selector-layout .user-selector-container.is-fast-clear { + padding-right: 22px; +} + +.user-selector .user-selector-layout .user-selector-container.disabled { + cursor: not-allowed; + background-color: #fafbfd !important; + border-color: #dcdee5 !important; +} + +.user-selector .user-selector-layout .user-selector-container.focus { + z-index: 1; + overflow: auto; + overflow-x: hidden; + overflow-y: auto; + white-space: normal; + border-color: #3a84ff; +} + +.user-selector .user-selector-layout .user-selector-container.focus::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.user-selector .user-selector-layout .user-selector-container.focus::-webkit-scrollbar-thumb { + background: #c4c6cc; + border-radius: 2px; + box-shadow: inset 0 0 6px rgb(204 204 204 / 30%); +} + +.user-selector .user-selector-layout .user-selector-container.placeholder::after { + position: absolute; + top: 0; + left: 0; + height: 100%; + padding: 0 0 0 10px; + font-size: 12px; + line-height: 30px; + color: #c3cdd7; + content: attr(data-placeholder); +} + +.user-selector .user-selector-layout .user-selector-container.has-avatar .user-selector-selected { + padding: 0; + margin: 4px 5px; + background: transparent; +} + +.user-selector .user-selector-layout .user-selector-container.has-avatar .user-selector-overflow-tag { + border-radius: 10px; +} + +.user-selector .user-selector-layout .user-selector-container.is-loading::before { + position: absolute; + inset: 0; + z-index: 2; + background-color: rgb(255 255 255 / 70%); + content: ""; +} + +.user-selector .user-selector-layout .user-selector-container.is-loading::after { + position: absolute; + top: 50%; + left: 50%; + z-index: 3; + width: 6px; + height: 6px; + margin-left: -24px; + border-radius: 50%; + content: ""; + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1; + animation: user-selector-loading 1s linear infinite; +} + +.user-selector .user-selector-layout .user-selector-overflow-count { + height: 22px; + min-width: 22px; + padding: 0 4px; + font-size: 12px; + line-height: 22px; + color: #63656e; + text-align: center; + background-color: #f0f1f5; +} + +.user-selector .user-selector-layout .user-selector-clear { + position: absolute; + top: 10px; + right: 5px; + z-index: 1; + font-size: 12px; + color: #c4c6cc; + cursor: pointer; +} + +.user-selector .user-selector-layout .user-selector-clear:hover { + color: #979ba5; +} + +.user-selector .user-selector-selected { + display: inline-flex; + max-width: 100%; + padding: 0 2px 0 4px; + margin: 4px 0 4px 6px; + font-size: 12px; + line-height: 22px; + vertical-align: top; + cursor: pointer; + background: #f0f1f5; + border-radius: 2px; + outline: 0; + align-items: center; +} + +.user-selector .user-selector-selected:hover { + background: #dcdee5; +} + +.user-selector .user-selector-selected .user-selector-selected-value { + overflow: hidden; + font-size: 12px; + color: #63656e; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.user-selector .user-selector-selected .user-selector-selected-clear { + height: 18px; + font-size: 18px; + line-height: 18px; + color: #979ba5; + text-align: center; + cursor: pointer; + flex: 18px 0 0; +} + +.user-selector .user-selector-selected .user-selector-selected-clear:hover { + color: #63656e; +} + +.user-selector .user-selector-input { + display: inline-block; + height: 22px; + max-width: 100%; + padding: 0 0 0 6px; + margin: 4px 0 0; + overflow: hidden; + font-size: 12px; + line-height: 22px; + white-space: nowrap; + vertical-align: top; + outline: none; +} + +.user-selector .user-selector-overflow-tag { + display: inline-flex; + min-width: 22px; + padding: 0 5px; + margin: 4px 0 4px 6px; + font-size: 12px; + line-height: 22px; + text-align: center; + background: #f0f1f5; +} + +.user-selector .user-selector-overflow-tag ~ .user-selector-selected { + pointer-events: none; + visibility: hidden; +} + +.user-selector .alternate-empty { + height: 32px; + padding: 0; + margin: 0; + line-height: 32px; + text-align: center; +} + +@keyframes user-selector-loading { + 0% { + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1; + } + + 14% { + box-shadow: 12px 0 0 1px #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1; + } + + 28% { + box-shadow: 12px 0 0 2px #fd6154, 24px 0 0 1px #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1; + } + + 42% { + box-shadow: 12px 0 0 1px #fd6154, 24px 0 0 2px #ffb726, 36px 0 0 1px #4cd084, 48px 0 0 0 #57a3f1; + } + + 56% { + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 1px #ffb726, 36px 0 0 2px #4cd084, 48px 0 0 1px #57a3f1; + } + + 70% { + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 1px #4cd084, 48px 0 0 2px #57a3f1; + } + + 84% { + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 1px #57a3f1; + } +} + +.user-selector-alternate-list-wrapper { + position: relative; + width: 190px; + color: #63656e; + background-color: #fff; +} + +.user-selector-alternate-list-wrapper.has-folder { + width: 300px; +} + +.user-selector-alternate-list-wrapper.is-loading { + min-height: 32px; +} + +.user-selector-alternate-list-wrapper.is-loading::before { + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + background-color: rgb(255 255 255 / 70%); + content: ''; +} + +.user-selector-alternate-list-wrapper.is-loading::after { + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + margin-left: -30px; + background-color: transparent; + border-radius: 50%; + content: ''; + box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1; + animation: user-selector-loading 1s linear infinite; +} + +.user-selector-alternate-list-wrapper .alternate-list { + max-height: 162px; + padding: 0; + margin: 0; + overflow-y: auto; + font-size: 12px; + line-height: 32px; + background: #fff; +} + +.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar-thumb { + background: #c4c6cc; + border-radius: 2px; + box-shadow: inset 0 0 6px rgb(204 204 204 / 30%); +} + +.user-selector-alternate-list-wrapper .alternate-item { + padding: 0 10px; + justify-content: space-between; + cursor: pointer; +} + +.user-selector-alternate-list-wrapper .alternate-item.highlight, .user-selector-alternate-list-wrapper .alternate-item:hover { + background-color: #f1f7ff; +} + +.user-selector-alternate-list-wrapper .alternate-item.disabled { + cursor: not-allowed; + opacity: 50%; +} + +.user-selector-alternate-list-wrapper .alternate-item .item-avatar { + float: left; + margin: 5px 8px 0 0; +} + +.user-selector-alternate-list-wrapper .alternate-item .item-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-selector-alternate-list-wrapper .alternate-item .item-name span { + color: #3a84ff; +} + +.user-selector-alternate-list-wrapper .alternate-item .item-folder { + float: right; + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + outline: 0; +} + +.user-selector-alternate-list-wrapper .alternate-group { + padding: 0 11px; + overflow: hidden; + color: #979ba5; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-selector-alternate-list-wrapper .alternate-empty { + padding: 0; + margin: 0; + font-size: 12px; + line-height: 44px; + text-align: center; +} + +.user-selector-avatar { + display: inline-block; + width: 22px; + height: 22px; + background-color: #eff0f5; + background-image: url('./icon-user.svg'); + background-position: center center; + background-repeat: no-repeat; + background-size: 100% 100%; + border-radius: 50%; +} + +.tippy-box[data-theme~="light"] { + font-size: 12px; + line-height: 24px; + color: #63656e; + border: 1px solid #dcdee5; + border-radius: 2px; + box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%); +} + +.tippy-box[data-theme~="light"][data-theme~="user-selected-tips"], .tippy-box[data-theme~="light"][data-theme~="list-item-tips"] { + padding: 0 9px; +} + +.tippy-box[data-theme~="light"][data-theme~="small-arrow"] > .tippy-arrow::before { + transform: scale(0.75); +} + +.tippy-content { + padding: 0; +} diff --git a/dbm-ui/frontend/patch/user-selector/tooltips.ts b/dbm-ui/frontend/patch/user-selector/tooltips.ts new file mode 100644 index 0000000000..56fe85926a --- /dev/null +++ b/dbm-ui/frontend/patch/user-selector/tooltips.ts @@ -0,0 +1,29 @@ +import Tippy, { type ReferenceElement } from 'tippy.js'; +import type { DirectiveBinding } from 'vue'; + +import 'tippy.js/dist/tippy.css'; +import 'tippy.js/themes/light.css'; +export default { + mounted(el: ReferenceElement, binding: DirectiveBinding) { + const props = typeof binding.value === 'object' + ? binding.value + : { disabled: false, content: binding.value }; + const instance = Tippy( + el, + Object.assign({ appendTo: document.body }, props), + ); + if (props.disabled) { + instance.disable(); + } + }, + unmounted(el: ReferenceElement) { + el._tippy?.destroy(); + }, + updated(el: ReferenceElement, binding: DirectiveBinding) { + const props = typeof binding.value === 'object' + ? binding.value + : { disabled: false, content: binding.value }; + props.disabled ? el._tippy.disable() : el._tippy.enable(); + el._tippy.setContent(props.content); + }, +}; diff --git a/dbm-ui/frontend/src/common/importComps.ts b/dbm-ui/frontend/src/common/importComps.ts index ac330079af..af8c543991 100644 --- a/dbm-ui/frontend/src/common/importComps.ts +++ b/dbm-ui/frontend/src/common/importComps.ts @@ -31,6 +31,7 @@ import MoreActionExtend from '@components/more-action-extend/Index.vue'; import SmartAction from '@components/smart-action/index.vue'; import { ipSelector } from '@components/vue2/ip-selector'; +import UserSelector from '@patch/user-selector/selector.vue'; export const setGlobalComps = (app: App) => { app.component('DbCard', DbCard); @@ -50,4 +51,5 @@ export const setGlobalComps = (app: App) => { app.component('I18nT', Translation); app.component('FunController', FunController); app.component('MoreActionExtend', MoreActionExtend); + app.component('UserSelector', UserSelector); }; diff --git a/dbm-ui/frontend/src/services/monitorAlarm.ts b/dbm-ui/frontend/src/services/monitorAlarm.ts index b2fafbfff3..802e62f436 100644 --- a/dbm-ui/frontend/src/services/monitorAlarm.ts +++ b/dbm-ui/frontend/src/services/monitorAlarm.ts @@ -18,11 +18,13 @@ import type { ListBase } from '@services/types/common'; interface AlarmGroup { id: number, name: string, + create_at: string, + creator: string, updater: string, update_at: string, bk_biz_id: number, monitor_group_id: number, - related_policy_count: number, + used_count: number, group_type: string, db_type: string, receivers: AlarmGroupRecivers[], diff --git a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue index 424f48e58a..38a8181405 100644 --- a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue +++ b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue @@ -32,7 +32,8 @@ ref="tableRef" class="alert-group-table" :columns="columns" - :data-source="getAlarmGroupList" /> + :data-source="getAlarmGroupList" + :row-class="setRowClass" /> + + diff --git a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue index 04d56b9016..8b0cf2fac7 100644 --- a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue +++ b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue @@ -46,13 +46,11 @@ {{ item.displayName }} - + width="400"> + +{{ overflowData.length }}