diff --git a/bcs-services/bcs-bscp/ui/index.html b/bcs-services/bcs-bscp/ui/index.html index 91a22afac5..a1342e553a 100644 --- a/bcs-services/bcs-bscp/ui/index.html +++ b/bcs-services/bcs-bscp/ui/index.html @@ -16,6 +16,7 @@ var GRPC_ADDR = '{{ .GRPC_ADDR }}'; var HTTP_ADDR = '{{ .HTTP_ADDR }}'; var BK_NODE_HOST = '{{ .BK_NODE_HOST }}'; + var USER_MAN_HOST = '{{ .USER_MAN_HOST }}'; window.BSCP_CONFIG = JSON.parse('{{ .BK_BSCP_CONFIG }}'); diff --git a/bcs-services/bcs-bscp/ui/package.json b/bcs-services/bcs-bscp/ui/package.json index 3af50fa394..192eb61fc6 100644 --- a/bcs-services/bcs-bscp/ui/package.json +++ b/bcs-services/bcs-bscp/ui/package.json @@ -15,8 +15,10 @@ "@blueking/login-modal": "^1.0.1", "@blueking/notice-component": "^2.0.1", "@blueking/platform-config": "^1.0.3", + "@icon-cool/bk-icon-bk-biz-components": "^0.0.4", "@types/js-cookie": "^3.0.2", "@types/lodash.clonedeep": "^4.5.7", + "@vitejs/plugin-vue-jsx": "^4.0.1", "axios": "^1.7.7", "bkui-vue": "1.0.3-beta.32", "crypto-js": "^4.2.0", @@ -30,6 +32,7 @@ "node-forge": "^1.3.1", "pinia": "^2.0.33", "sass": "^1.54.8", + "tippy.js": "^6.3.7", "vue": "^3.4.21", "vue-i18n": "9", "vue-router": "^4.1.5", diff --git a/bcs-services/bcs-bscp/ui/src/api/config.ts b/bcs-services/bcs-bscp/ui/src/api/config.ts index 5df606e075..5c1ee1d1e0 100644 --- a/bcs-services/bcs-bscp/ui/src/api/config.ts +++ b/bcs-services/bcs-bscp/ui/src/api/config.ts @@ -274,6 +274,30 @@ export const publishVersion = ( }, ) => http.post(`/config/update/strategy/publish/publish/release_id/${releaseId}/app_id/${appId}/biz_id/${bizId}`, data); +/** + * 发布版本(增加审批) + * @param bizId 业务ID + * @param appId 应用ID + * @param data 参数 + * @param publish_type 上线方式 + * @param publish_time 定时上线时间 + * @param is_compare 所有待上线的分组是否为首次上线 + * @returns + */ +export const publishVerSubmit = ( + bizId: string, + appId: number, + releaseId: number, + data: { + groups: Array; + all: boolean; + memo: string; + publish_type: 'Manually' | 'Automatically' | 'Periodically' | 'Immediately' | ''; + publish_time: Date | string; + is_compare: boolean; + }, +) => http.post(`/config/biz_id/${bizId}/app_id/${appId}/release_id/${releaseId}/submit`, data); + /** * 获取服务下初始化脚本引用配置 * @param bizId 业务ID @@ -675,3 +699,32 @@ export const createVersionNameCheck = (bizId: string, appId: number, name: strin */ export const importConfigFromTemplate = (bizId: string, appId: number, query: any) => http.post(`/config/biz/${bizId}/apps/${appId}/template_bindings/import_template_set`, query); + +/** + * 上次上线方式查询 + * @param bizId 业务ID + * @param appId 应用ID + * @returns + */ +export const publishType = (bizId: string, appId: number) => + http.get(`/config/biz_id/${bizId}/app_id/${appId}/last/select`); + +/** + * 当前版本状态查询 + * @param bizId 业务ID + * @param appId 应用ID + * @param releaseId 版本ID + * @returns + */ +export const versionStatusQuery = (bizId: string, appId: number, releaseId: number) => + http.get(`/config/biz_id/${bizId}/app_id/${appId}/release_id/${releaseId}/status`); + +/** + * 当前服务下 所有版本上线状态检查 + * @param bizId 业务ID + * @param appId 应用ID + * @param releaseId 版本ID + * @returns + */ +export const versionStatusCheck = (bizId: string, appId: number) => + http.get(`/config/biz_id/${bizId}/app_id/${appId}/last/publish`); diff --git a/bcs-services/bcs-bscp/ui/src/api/index.ts b/bcs-services/bcs-bscp/ui/src/api/index.ts index 6ec63b509e..4603193b93 100644 --- a/bcs-services/bcs-bscp/ui/src/api/index.ts +++ b/bcs-services/bcs-bscp/ui/src/api/index.ts @@ -114,3 +114,11 @@ export const loginOut = () => http.get('/logout').then((resp) => { window.location.href = `${resp.data.login_url}${encodeURIComponent(window.location.href)}&is_from_logout=1`; }); + +/** + * 审批人员名单 + * @returns + */ +export const getApproverListApi = () => + `${(window as any).USER_MAN_HOST}/api/c/compapi/v2/usermanage/fs_list_users/?app_code=bk-magicbox&page_size=1000&page=1`; +// /api/c/compapi/v2/usermanage/fs_list_users/?app_code=bk-magicbox&page_size=1000&page=1" diff --git a/bcs-services/bcs-bscp/ui/src/api/record.ts b/bcs-services/bcs-bscp/ui/src/api/record.ts new file mode 100644 index 0000000000..efc4b9ade6 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/api/record.ts @@ -0,0 +1,29 @@ +import http from '../request'; +import { IRecordQuery } from '../../types/record'; + +/** + * 获取操作记录列表 + * @param biz_id 空间ID + * @param params 查询参数 + * @returns + */ +export const getRecordList = (biz_id: string, params: IRecordQuery) => + http.get(`/config/biz_id/${biz_id}/audits`, { params }).then((res) => res.data); + +/** + * 审批操作:撤销/驳回/通过/手动上线 + * @param biz_id 空间ID + * @param app_id 服务ID + * @param release_id 版本ID + * @param params 参数 + * @returns + */ +export const approve = ( + biz_id: string, + app_id: number, + release_id: number, + params: { publish_status: string; reason?: string }, +) => + http + .post(`/config/biz_id/${biz_id}/app_id/${app_id}/release_id/${release_id}/approve`, { ...params }) + .then((res) => res.data); diff --git a/bcs-services/bcs-bscp/ui/src/components/head.vue b/bcs-services/bcs-bscp/ui/src/components/head.vue index 1e63e5fe31..cc89c423e8 100644 --- a/bcs-services/bcs-bscp/ui/src/components/head.vue +++ b/bcs-services/bcs-bscp/ui/src/components/head.vue @@ -177,6 +177,7 @@ { id: 'configuration-example', module: 'example', name: t('配置示例') }, ], }, + { id: 'records-all', module: 'records', name: t('操作记录') }, ]); const optionList = ref([]); diff --git a/bcs-services/bcs-bscp/ui/src/components/user-selector/index.vue b/bcs-services/bcs-bscp/ui/src/components/user-selector/index.vue new file mode 100644 index 0000000000..1db30c01f4 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/index.vue @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/alternate-item.tsx b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/alternate-item.tsx new file mode 100644 index 0000000000..150de4d9b6 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/alternate-list.tsx b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/alternate-list.tsx new file mode 100644 index 0000000000..e195ea3b39 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/alternate-list.tsx @@ -0,0 +1,168 @@ +// @ts-nocheck +/* eslint-disable */ +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 ( + + void this.handleScroll()} + > + { + this.matchedUsers.map((user, index) => { + if (user.hasOwnProperty('children')) { + return <> + e.stopPropagation()} + onMousedown={withModifiers((): any => void this.selector.handleGroupMousedown(), ['left', 'stop'])} + onMouseup={withModifiers((): any => void this.selector.handleGroupMouseup(), ['left', 'stop'])} + > + { `${user.display_name}(${user.children.length})` } + + { + user.children.map((child: any, childIndex: number) => <> + void this.setRef(el)} + index={this.getIndex(index, childIndex)} + selector={this.selector} + user={child} + keyword={this.keyword} /> + >) + } + >; + } + return <> + void this.setRef(el)} + selector={this.selector} + user={user} + index={this.getIndex(index)} + keyword={this.keyword} /> + >; + }) + } + + { + (!this.loading && !this.matchedUsers.length) + ? <> + + { this.selector.emptyText } + + > + : null + } + + ); + }, +}); diff --git a/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/icon-user.svg b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/icon-user.svg new file mode 100644 index 0000000000..4bdf4f533d --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/icon-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/index.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/index.ts new file mode 100644 index 0000000000..250dbc4e80 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/instance-store.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/instance-store.ts new file mode 100644 index 0000000000..fedc963480 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-alternate.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-alternate.ts new file mode 100644 index 0000000000..3d7cc39a91 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-avatar.tsx b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-avatar.tsx new file mode 100644 index 0000000000..886945aaa1 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-list.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-list.ts new file mode 100644 index 0000000000..2712e201f8 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-tag.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/render-tag.ts new file mode 100644 index 0000000000..113f18216d --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/request.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/request.ts new file mode 100644 index 0000000000..02e0bc7f23 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/request.ts @@ -0,0 +1,231 @@ +// @ts-nocheck +/* eslint-disable */ +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, + }; + }, + render() { + return null; // 渲染为空 + }, + }; + + 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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/selector.vue b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/selector.vue new file mode 100644 index 0000000000..2bbc4b521d --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/selector.vue @@ -0,0 +1,1204 @@ + + + + + + + + + + + + + + + + + {{ getDisplayText(user) }} + + + + + + + + + + + + + + + + {{ userInfo }} + + + + diff --git a/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/style.css b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/style.css new file mode 100644 index 0000000000..b22a084988 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/style.css @@ -0,0 +1,401 @@ +.user-selector * { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.user-selector { + display: inline-block; + min-width: 120px; + font-size: 14px; + cursor: text; + color: #63656e; +} + +.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; + line-height: 1; + border: 1px solid #c4c6cc; + border-radius: 2px; + background-color: #fff; + font-size: 0; + overflow: hidden; +} + +.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 { + overflow: auto; + overflow-x: hidden; + overflow-y: auto; + white-space: normal; + border-color: #3a84ff; + z-index: 1; +} + +.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 { + border-radius: 2px; + background: #c4c6cc; + box-shadow: inset 0 0 6px rgba(204, 204, 204, .3); +} + +.user-selector .user-selector-layout .user-selector-container.placeholder:after { + position: absolute; + left: 0; + top: 0; + height: 100%; + padding: 0 0 0 10px; + line-height: 30px; + content: attr(data-placeholder); + font-size: 12px; + color: #c3cdd7; +} + +.user-selector .user-selector-layout .user-selector-container.has-avatar .user-selector-selected { + background: transparent; + padding: 0; + margin: 4px 5px; +} + +.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 { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(255, 255, 255, .7); + z-index: 2; +} + +.user-selector .user-selector-layout .user-selector-container.is-loading:after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + margin-left: -24px; + border-radius: 50%; + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 0px #57a3f1; + animation: user-selector-loading 1s linear infinite; + z-index: 3; +} + +.user-selector .user-selector-layout .user-selector-overflow-count { + min-width: 22px; + height: 22px; + line-height: 22px; + text-align: center; + padding: 0 4px; + background-color: #f0f1f5; + font-size: 12px; + color: #63656e; +} + +.user-selector .user-selector-layout .user-selector-clear { + position: absolute; + top: 10px; + right: 5px; + font-size: 12px; + color: #c4c6cc; + cursor: pointer; + z-index: 1; +} + +.user-selector .user-selector-layout .user-selector-clear:hover { + color: #979ba5; +} + +.user-selector .user-selector-selected { + display: inline-flex; + max-width: 100%; + align-items: center; + vertical-align: top; + margin: 4px 0 4px 6px; + padding: 0 2px 0 4px; + border-radius: 2px; + background: #f0f1f5; + line-height: 22px; + outline: 0; + font-size: 12px; + cursor: pointer; +} + +.user-selector .user-selector-selected:hover { + background: #dcdee5; +} + +.user-selector .user-selector-selected .user-selector-selected-value { + flex: 1; + font-size: 12px; + color: #63656e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-selector .user-selector-selected .user-selector-selected-clear { + flex: 18px 0 0; + height: 18px; + line-height: 18px; + text-align: center; + color: #979ba5; + font-size: 18px; + cursor: pointer; +} + +.user-selector .user-selector-selected .user-selector-selected-clear:hover { + color: #63656e; +} + +.user-selector .user-selector-input { + display: inline-block; + vertical-align: top; + max-width: 100%; + height: 22px; + margin: 4px 0 0; + padding: 0 0 0 6px; + white-space: nowrap; + line-height: 22px; + font-size: 12px; + outline: none; + overflow: hidden; +} + +.user-selector .user-selector-overflow-tag { + display: inline-flex; + padding: 0 5px; + margin: 4px 0 4px 6px; + min-width: 22px; + line-height: 22px; + font-size: 12px; + text-align: center; + background: #f0f1f5; +} + +.user-selector .user-selector-overflow-tag ~ .user-selector-selected { + visibility: hidden; + pointer-events: none; +} + +.user-selector .alternate-empty { + height: 32px; + padding: 0; + margin: 0; + text-align: center; + line-height: 32px; +} + +@keyframes user-selector-loading { + 0% { + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 0px #57a3f1; + } + + 14% { + box-shadow: 12px 0px 0px 1px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 0px #57a3f1; + } + + 28% { + box-shadow: 12px 0px 0px 2px #fd6154, 24px 0px 0px 1px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 0px #57a3f1; + } + + 42% { + box-shadow: 12px 0px 0px 1px #fd6154, 24px 0px 0px 2px #ffb726, 36px 0px 0px 1px #4cd084, 48px 0px 0px 0px #57a3f1; + } + + 56% { + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 1px #ffb726, 36px 0px 0px 2px #4cd084, 48px 0px 0px 1px #57a3f1; + } + + 70% { + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 1px #4cd084, 48px 0px 0px 2px #57a3f1; + } + + 84% { + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 1px #57a3f1; + } +} + +.user-selector-alternate-list-wrapper { + width: 190px; + color: #63656e; + position: relative; + 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 { + content: ''; + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, .7); + z-index: 1; +} + +.user-selector-alternate-list-wrapper.is-loading:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + margin-left: -30px; + border-radius: 50%; + background-color: transparent; + box-shadow: 12px 0px 0px 0px #fd6154, 24px 0px 0px 0px #ffb726, 36px 0px 0px 0px #4cd084, 48px 0px 0px 0px #57a3f1; + animation: user-selector-loading 1s linear infinite; +} + +.user-selector-alternate-list-wrapper .alternate-list { + margin: 0; + padding: 0; + max-height: 162px; + font-size: 12px; + line-height: 32px; + background: #fff; + overflow-y: auto; +} + +.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar-thumb { + border-radius: 2px; + background: #c4c6cc; + box-shadow: inset 0 0 6px rgba(204, 204, 204, .3); +} + +.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 { + opacity: 0.5; + cursor: not-allowed; +} + +.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; + outline: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-selector-alternate-list-wrapper .alternate-group { + padding: 0 11px; + color: #979ba5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-selector-alternate-list-wrapper .alternate-empty { + padding: 0; + margin: 0; + text-align: center; + line-height: 44px; + font-size: 12px; +} + +.user-selector-avatar { + display: inline-block; + width: 22px; + height: 22px; + border-radius: 50%; + background-color: #eff0f5; + background-repeat: no-repeat; + background-position: center center; + background-size: 100% 100%; + background-image: url('./icon-user.svg'); +} + +.tippy-box[data-theme~=light] { + border: 1px solid #dcdee5; + border-radius: 2px; + box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1); + color: #63656e; + font-size: 12px; + line-height: 24px; +} + +.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/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/tooltips.ts b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/tooltips.ts new file mode 100644 index 0000000000..6f4ef7bcb0 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/components/user-selector/user-selector-origin/tooltips.ts @@ -0,0 +1,31 @@ +// @ts-nocheck +/* eslint-disable */ +import type { DirectiveBinding } from 'vue'; + +import Tippy, { ReferenceElement } from 'tippy.js'; +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/bcs-services/bcs-bscp/ui/src/constants/config.ts b/bcs-services/bcs-bscp/ui/src/constants/config.ts index 9614dddd70..6181be7b54 100644 --- a/bcs-services/bcs-bscp/ui/src/constants/config.ts +++ b/bcs-services/bcs-bscp/ui/src/constants/config.ts @@ -68,3 +68,28 @@ export const GET_UNNAMED_VERSION_DATA = (): IConfigVersion => ({ fully_released: false, }, }); + +// 版本上线格式 +export enum APPROVE_TYPE { + PendApproval, // 0 待审批 + PendPublish, // 1 审批通过 + Rejected, // 2 驳回 + Revoke, // 3 撤销 +} + +// 版本上线方式 +export enum ONLINE_TYPE { + Manually = 'Manually', // 手动上线 + Automatically = 'Automatically', // 审批通过后自动上线 + Periodically = 'Periodically', // 定时上线 + Immediately = 'Immediately', // 立即上线 +} + +// 版本状态 +export enum APPROVE_STATUS { + PendApproval = 'PendApproval', // 待审批 + PendPublish = 'PendPublish', // 待上线 + RevokedPublish = 'RevokedPublish', // 撤销上线 + RejectedApproval = 'RejectedApproval', // 审批驳回 + AlreadyPublish = 'AlreadyPublish', // 已上线 +} diff --git a/bcs-services/bcs-bscp/ui/src/constants/record.ts b/bcs-services/bcs-bscp/ui/src/constants/record.ts new file mode 100644 index 0000000000..471deb1e52 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/constants/record.ts @@ -0,0 +1,59 @@ +import { localT } from '../i18n'; + +// 资源类型 +export const RECORD_RES_TYPE = { + app_config: localT('服务配置'), // 2024.9 第一版只有这个字段 +}; + +// 操作行为 +export const ACTION = { + Create: localT('创建服务'), + Publish: localT('上线服务'), + Update: localT('更新服务'), + Delete: localT('删除服务'), + PublishVersionConfig: localT('上线版本配置'), +}; + +// 资源实例 +export const INSTANCE = { + releases_name: localT('配置版本名称'), + group: localT('配置上线范围'), +}; + +// 状态 +export const STATUS = { + PendApproval: localT('待审批'), + PendPublish: localT('待上线'), + RevokedPublish: localT('撤销上线'), + RejectedApproval: localT('审批驳回'), + AlreadyPublish: localT('已上线'), + Failure: localT('失败'), + Success: localT('成功'), +}; + +// 版本状态 +export enum APPROVE_STATUS { + PendApproval = 'PendApproval', // 待审批 + PendPublish = 'PendPublish', // 待上线 + RevokedPublish = 'RevokedPublish', // 撤销上线 + RejectedApproval = 'RejectedApproval', // 审批驳回 + AlreadyPublish = 'AlreadyPublish', // 已上线 + Failure = 'Failure', + Success = 'Success', +} + +// 过滤的Key +export enum FILTER_KEY { + PublishVersionConfig = 'PublishVersionConfig', // 上线版本配置 + Failure = 'Failure', // 失败 +} + +export enum SEARCH_ID { + resource_type = 'resource_type', // 资源类型 + action = 'action', // 操作行为 + status = 'status', // 状态 + // service = 'service', // 所属服务 + res_instance = 'res_instance', // 资源实例 + operator = 'operator', // 操作人 + operate_way = 'operate_way', // 操作途径 +} diff --git a/bcs-services/bcs-bscp/ui/src/i18n/en-us.ts b/bcs-services/bcs-bscp/ui/src/i18n/en-us.ts index 6224fa74e6..1f2e582c7f 100644 --- a/bcs-services/bcs-bscp/ui/src/i18n/en-us.ts +++ b/bcs-services/bcs-bscp/ui/src/i18n/en-us.ts @@ -104,6 +104,7 @@ export default { 配置示例: 'Configuration Example', 业务名: 'Business Name', 业务: 'Business', + 操作记录: 'Operation Log', // 配置管理 版本名称: 'Version Name', @@ -320,7 +321,6 @@ export default { 本次上线版本对以下分组实例: 'his version update will ', 不会产生影响: 'not affect the following group instances', 对比并上线: 'Compare and go online', - 版本已上线: 'Version is online', 请选择分组实例: 'Please select an online group', 本次上线分组: 'This online group', 上线说明: 'Online instructions', @@ -481,6 +481,67 @@ export default { '上传后,该服务的配置文件数量将达到 {n} 个,超过了最大限制': 'After uploading, the number of configuration files in the service will reach {n}, exceeding the maximum limit', '文件上传准备中,请稍候…': 'File upload is in preparation, please wait...', '( 后台已存在此文件,上传快速完成 )': '( The background already has this file, the upload is completed quickly )', + 审批开启的文案: 'This service version has enabled the approval process for release. Please select the release method after approval\nManual Publish: Suitable for scenarios requiring human intervention and confirmation, commonly used for module\nconfigurations in critical or formal environments\nGo live immediately after approval: Suitable for highly automated scenarios, often used for configurations in non-formal\nenvironments to enhance deployment efficiency\nScheduled Publish: Suitable for scenarios requiring a release at a specific time; if the approval process is not completed\nbefore the specified time, it will automatically switch to Manual Publish', + 审批关闭的文案: 'Immediate Publish: After clicking "Confirm Publish," the configuration version will be published immediately,\nsuitable for urgent updates and other scenarios that require immediate effect.\nScheduled Publish: After clicking "Confirm Publish," the configuration version will not take effect immediately,\n and a specific time can be set for the version to be automatically published, suitable\n for non-urgent update scenarios, avoiding peak times for publishing and reducing user impact', + 指定审批人不能为空: 'The designated approver cannot be empty', + 提交上线审批: 'Submit Publish Approval', + 不能选择过去的时间: 'Cannot select a past time', + 上线方式: 'Publish Method', + 立即上线: 'Immediate Publish', + 定时上线: 'Scheduled Publish', + 手动上线: 'Manual Publish', + 审批通过后立即上线: 'Go live immediately after approval', + 已过时: 'Outdated', + 定时上线文案: 'The version will be released on {time}', + '定时上线文案-调整分组': 'Adjustment group version will take effect at {time}', + '手动上线文案-调整分组': 'After approval, manual action is required to adjust the grouping and deploy it online', + '审批通过后上线文案-调整分组': 'After approval, the grouping adjustment will go live automatically', + 手动上线文案: 'After Approval, The Version Will Require Manual Publishing', + 审批通过后上线文案: 'After Approval, The Version will be automatically published', + 版本已上线: 'Version has been published', + 等待定时上线: 'Scheduled for Release', + 待审批: 'Pending approval', + 审批驳回: 'Approval rejected', + 撤销上线: 'Revoke publish', + 审批通过: 'Approval passed', + 待上线: 'Pending publish', + 操作时间: 'Operation time', + 所属服务: 'Belonging service', + 资源类型: 'Resource type', + 操作行为: 'Operation behavior', + 资源实例: 'Resource instance', + 操作人: 'Operator', + 操作途径: 'Operation Method', + 服务配置: 'Service configuration', + 创建服务: 'Create service', + 更新服务: 'Update service', + 删除服务: 'Delete service', + 上线版本配置: 'Version Configuration for Release', + '提示-已上线文案': 'Publish operator : {reviser}\nPublish Time: {time}', + '提示-审批驳回': 'Approval has been rejected\nApprover: {reviser}\nPublish time: {time}\nRejection reason: {reason}', + '提示-已撤销': 'Has been withdrawn\nWithdrawn by: {reviser}\nWithdrawal time: {time}', + '提示-失败': 'This script version is currently being used by the service, deletion failed', + 配置版本名称: 'Configuration version name', + 配置上线范围: 'Configuration publish scope', + 去审批: 'Go to approval', + 审批: 'Approval', + 审批人: 'Approver', + 撤销人: 'Revoker', + 再次提交: 'Submit again', + 确认驳回该上线任务: 'Confirm rejection of this publish task', + '资源类型/操作行为/资源实例/状态/操作人/操作途径': 'Resource type/Operation behavior/Resource instance/Status/Operator/Operation method', + 仅看上线操作: 'View Only Online Operations', + 仅看失败操作: 'View Only Failed Operations', + 驳回理由: 'Reasons for rejection', + 驳回: 'Rejected', + 通过: 'Approved', + 确认撤销该上线任务: 'Confirm to revoke this publish task', + 确认上线该版本: 'Confirm publishing this version', + 服务: 'Service', + 上线范围: 'Scope of publish', + 服务上线记录: 'Service Publish Log', + 操作成功: 'Success', + 查看全部配置项: 'View all configuration items', 只看冲突配置项: 'Only view conflict configuration items ', '已限制该服务下所有配置项数据类型为{n},如需其他数据类型,请调整服务属性下的数据类型': 'All configuration item data types under the service are limited to {n}, if you need other data types, please adjust the data type under the service attributes', diff --git a/bcs-services/bcs-bscp/ui/src/i18n/zh-cn.ts b/bcs-services/bcs-bscp/ui/src/i18n/zh-cn.ts index e8e576cf68..4f9140f565 100644 --- a/bcs-services/bcs-bscp/ui/src/i18n/zh-cn.ts +++ b/bcs-services/bcs-bscp/ui/src/i18n/zh-cn.ts @@ -70,6 +70,10 @@ export default { 请输入服务名: '请输入服务名', 同时会删除服务密钥对服务的关联规则: '同时会删除服务密钥对服务的关联规则', 删除服务成功: '删除服务成功', + 或签: '或签', + 会签: '会签', + '建议在生产环境中开启审批流程,以保证系统稳定性。测试环境中可以考虑关闭审批流程以提升操作效率': '建议在生产环境中开启审批流程,以保证系统稳定性。测试环境中可以考虑关闭审批流程以提升操作效率', + '或签:多人同时审批,一人同意即可通过n会签:审批人依次审批,每人都需同意才能通过': '或签:多人同时审批,一人同意即可通过\n会签:审批人依次审批,每人都需同意才能通过', 敏感信息: '敏感信息', 请选择数据类型: '请选择数据类型', 服务属性编辑成功: '服务属性编辑成功', @@ -100,6 +104,7 @@ export default { 配置示例: '配置示例', 业务名: '业务名', 业务: '业务', + 操作记录: '操作记录', // 配置管理 @@ -319,7 +324,6 @@ export default { 本次上线版本对以下分组实例: '本次上线版本对以下分组实例', 不会产生影响: '不会产生影响', 对比并上线: '对比并上线', - 版本已上线: '版本已上线', 请选择分组实例: '请选择分组实例', 本次上线分组: '本次上线分组', 上线说明: '上线说明', @@ -479,6 +483,70 @@ export default { '上传后,该服务的配置文件数量将达到 {n} 个,超过了最大限制': '上传后,该服务的配置文件数量将达到 {n} 个,超过了最大限制', '文件上传准备中,请稍候…': '文件上传准备中,请稍候…', '( 后台已存在此文件,上传快速完成 )': '( 后台已存在此文件,上传快速完成 )', + 审批开启的文案: '此服务版本上线已启用审批流程,请选择审批通过后的上线方式\n手动上线:适用于需人工干预和确认的场景,常用于重要业务或正式环\n境中的模块配置上线\n审批通过后立即上线:适用于高度自动化的场景,常用于非正式环境的\n配置上线,以提升部署效率\n定时上线:适用于需在特定时间上线的场景;若在指定时间前审批流程\n未完成,则将自动切换为手动上线', + 审批关闭的文案: '立即上线:点击“确认上线”后,配置版本将立即上线,\n适用于紧急更新等立即生效的场景\n定时上线:点击“确认上线”后,配置版本不会立即生\n效,可设定具体时间点,使版本在该时间自动上线,适用\n于非紧急更新场景,避免高峰时段上线,降低用户影响', + 指定审批人不能为空: '指定审批人不能为空', + 提交上线审批: '提交上线审批', + 不能选择过去的时间: '不能选择过去的时间', + 上线方式: '上线方式', + 立即上线: '立即上线', + 定时上线: '定时上线', + 手动上线: '手动上线', + 审批通过后立即上线: '审批通过后立即上线', + 已过时: '已过时', + 定时上线文案: '版本将于 {time} 上线', + '定时上线文案-调整分组': '调整分组上线版本将于 {time} 生效', + '手动上线文案-调整分组': '待审批通过后, {text}需手动进行上线操作', + '审批通过后上线文案-调整分组': '待审批通过后, {text}将自动上线', + 手动上线文案: '待审批通过后, 版本需手动进行上线操作', + 审批通过后上线文案: '待审批通过后, 版本将自动上线', + 版本已上线: '版本已上线', + 等待定时上线: '等待定时上线', + 待审批: '待审批', + 审批驳回: '审批驳回', + 撤销上线: '撤销上线', + 审批通过: '审批通过', + 待上线: '待上线', + 操作时间: '操作时间', + 所属服务: '所属服务', + 资源类型: '资源类型', + 操作行为: '操作行为', + 资源实例: '资源实例', + 操作人: '操作人', + 操作途径: '操作途径', + // 状态: '状态', + // 操作: '操作', + 服务配置: '服务配置', + 创建服务: '创建服务', + // 上线服务: '上线服务', + 更新服务: '更新服务', + 删除服务: '删除服务', + 上线版本配置: '上线版本配置', + '提示-已上线文案': '上线操作人:{reviser}\n上线时间:{time}', + '提示-审批驳回': '审批已驳回\n审批人:{reviser}\n上线时间:{time}\n驳回原因:{reason}', + '提示-已撤销': '已撤销\n撤销人:{reviser}\n撤销时间:{time}', + '提示-失败': '此脚本版本正在被服务使用,删除失败', + 配置版本名称: '配置版本名称', + 配置上线范围: '配置上线范围', + 去审批: '去审批', + 审批: '审批', + 审批人: '审批人', + 撤销人: '撤销人', + 再次提交: '再次提交', + 确认驳回该上线任务: '确认驳回该上线任务', + 确认上线该版本: '确认上线该版本', + '资源类型/操作行为/资源实例/状态/操作人/操作途径': '资源类型/操作行为/资源实例/状态/操作人/操作途径', + 仅看上线操作: '仅看上线操作', + 仅看失败操作: '仅看失败操作', + 驳回理由: '驳回理由', + 驳回: '驳回', + 通过: '通过', + 确认撤销该上线任务: '确认撤销该上线任务', + 服务: '服务', + 上线范围: '上线范围', + 服务上线记录: '服务上线记录', + 操作成功: '操作成功', + '已限制该服务下所有配置项数据类型为{n},如需其他数据类型,请调整服务属性下的数据类型': '已限制该服务下所有配置项数据类型为{n},如需其他数据类型,请调整服务属性下的数据类型', 敏感信息不可见: '敏感信息不可见', '「敏感信息不可见」启用提示': '「敏感信息不可见」启用提示', diff --git a/bcs-services/bcs-bscp/ui/src/request/index.ts b/bcs-services/bcs-bscp/ui/src/request/index.ts index 0d5c4de29d..7e91a99b2e 100644 --- a/bcs-services/bcs-bscp/ui/src/request/index.ts +++ b/bcs-services/bcs-bscp/ui/src/request/index.ts @@ -5,6 +5,9 @@ import pinia from '../store/index'; import useGlobalStore from '../store/global'; const http = axios.create({ + headers: { + 'X-Bscp-Operate-Way': 'WebUI', + }, baseURL: `${(window as any).BK_BCS_BSCP_API}/api/v1`, withCredentials: true, }); diff --git a/bcs-services/bcs-bscp/ui/src/router.ts b/bcs-services/bcs-bscp/ui/src/router.ts index 7029384ebd..d241857734 100644 --- a/bcs-services/bcs-bscp/ui/src/router.ts +++ b/bcs-services/bcs-bscp/ui/src/router.ts @@ -166,6 +166,27 @@ const routes = [ }, component: () => import('./views/space/client/example/index.vue'), }, + { + path: 'records', + children: [ + { + path: 'all', + name: 'records-all', + component: () => import('./views/space/records/index.vue'), + meta: { + navModule: 'records', + }, + }, + { + path: ':appId(\\d+)', + name: 'records-app', + component: () => import('./views/space/records/index.vue'), + meta: { + navModule: 'records', + }, + }, + ], + }, ], }, { diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/date-picker.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/date-picker.vue new file mode 100644 index 0000000000..484ccafd23 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/date-picker.vue @@ -0,0 +1,119 @@ + + + + + + 确定 + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-confirm.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-confirm.vue new file mode 100644 index 0000000000..61156f1261 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-confirm.vue @@ -0,0 +1,165 @@ + + + + + {{ dialogType === 'publish' ? `${t('确认上线该版本')}?` : `${t('确认撤销该上线任务')}?` }} + + + + + {{ $t('服务') }}: + {{ data.service || '--' }} + + + {{ t('待上线版本') }}: + {{ data.version || '--' }} + + + {{ t('上线范围') }}: + {{ data.group || '--' }} + + + + {{ t('说明') }} + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-reject.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-reject.vue new file mode 100644 index 0000000000..3a3d4d91dc --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/dialog-reject.vue @@ -0,0 +1,193 @@ + + + + + + + {{ t('确认驳回该上线任务') }}? + + + + {{ t('待上线版本') }}: + {{ releaseName || '--' }} + + + + {{ t('驳回理由') }} + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/more-actions.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/more-actions.vue new file mode 100644 index 0000000000..ca043db12c --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/more-actions.vue @@ -0,0 +1,84 @@ + + + + + {{ $t('撤销') }} + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/record-table.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/record-table.vue new file mode 100644 index 0000000000..6e20624c6d --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/record-table.vue @@ -0,0 +1,651 @@ + + + + + + + + {{ row.audit?.revision.created_at }} + + + + {{ row.app?.name || '--' }} + + + + {{ RECORD_RES_TYPE[row.audit?.spec.res_type as keyof typeof RECORD_RES_TYPE] || '--' }} + + + + + {{ ACTION[row.audit?.spec.action as keyof typeof ACTION] || '--' }} + + + + + + -- + + + + + + {{ row.audit?.spec.operator || '--' }} + + + + {{ row.audit?.spec.operate_way || '--' }} + + + + + + {{ STATUS[row.audit.spec.status as keyof typeof STATUS] || '--' }} + + + + + + -- + + + + + + + + + {{ t('上线') }} + + + + + + + + {{ t('去审批') }} + + + + {{ t('审批') }} + + + + + {{ t('再次提交') }} + + -- + + + + -- + + + + + + + + + + + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/search-option.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/search-option.vue new file mode 100644 index 0000000000..a3e17ac176 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/search-option.vue @@ -0,0 +1,224 @@ + + + {{ $t('仅看上线操作') }} + {{ $t('仅看失败操作') }} + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/service-selector.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/service-selector.vue new file mode 100644 index 0000000000..2ebd527905 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/service-selector.vue @@ -0,0 +1,190 @@ + + + + + + {{ localApp.name }} + + {{ $t('暂无服务') }} + + + + + + {{ t('全部服务') }} + + + + + {{ item.spec.name }} + + {{ item.spec.config_type === 'file' ? $t('文件型') : $t('键值型') }} + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/components/version-diff.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/components/version-diff.vue new file mode 100644 index 0000000000..e64f32e384 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/components/version-diff.vue @@ -0,0 +1,216 @@ + + + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/records/index.vue b/bcs-services/bcs-bscp/ui/src/views/space/records/index.vue new file mode 100644 index 0000000000..c9dcb33187 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/records/index.vue @@ -0,0 +1,79 @@ + + + + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/detail-header.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/detail-header.vue index fc2620520d..64eec6f61d 100644 --- a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/detail-header.vue +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/detail-header.vue @@ -40,6 +40,7 @@ + + + + @@ -77,6 +86,8 @@ import PublishVersion from './publish-version/index.vue'; import CreateVersion from './create-version/index.vue'; import ModifyGroupPublish from './modify-group-publish.vue'; + import HeaderMoreOptions from './header-more-options.vue'; + import VersionApproveStatus from './version-approve-status.vue'; const route = useRoute(); const router = useRouter(); @@ -91,6 +102,7 @@ create: false, publish: false, }); + const verAppStatus = ref(); const props = defineProps<{ bkBizId: string; @@ -103,6 +115,18 @@ { name: 'script', label: t('前/后置脚本'), routeName: 'init-script' }, ]); + const approveData = ref<{ + status: string; + time: string; + type: string; + }>({ + status: '', + time: '', + type: '', + }); + + const creator = ref(''); + const getDefaultTab = () => { const tab = tabs.value.find((item) => item.routeName === route.name); return tab ? tab.name : 'config'; @@ -165,6 +189,11 @@ getVersionPerms(); }); + const getVerApproveStatus = (approveStatusData: any, creatorData: string) => { + approveData.value = approveStatusData; + creator.value = creatorData; + }; + const getVersionPerms = async () => { permCheckLoading.value = true; const [createRes, publishRes] = await Promise.all([ diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/dialog-publish-warn.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/dialog-publish-warn.vue new file mode 100644 index 0000000000..70ccc23589 --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/dialog-publish-warn.vue @@ -0,0 +1,245 @@ + + + + + + + + {{ dialogType === 'confirm' ? '当前服务有正在上线的任务,请稍后尝试' : '高频上线风险提示' }} + + + + 正在上线的版本: + + {{ dialogData }} + + + + + + 距上次版本上线不到 2 小时 + ,请确保当前情况确实需要上线版本,避免过于频繁的上线操作可能带来的潜在风险 + + + 近三次版本上线记录 + + 查看全部上线记录 + + + + + + 上线时间 + 上线版本 + 上线范围 + 操作人 + + + {{ item.publish_time || '--' }} + {{ item.name || '--' }} + {{ item.fully_released ? '全部实例' : versionScope(item.scope.groups) }} + {{ item.creator || '--' }} + + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/header-more-options.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/header-more-options.vue new file mode 100644 index 0000000000..702769586c --- /dev/null +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/header-more-options.vue @@ -0,0 +1,145 @@ + + + + + {{ $t('服务上线记录') }} + + + {{ $t('撤销') }} + + + + + + + + + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/modify-group-publish.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/modify-group-publish.vue index fe35f69695..3020a9802e 100644 --- a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/modify-group-publish.vue +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/components/modify-group-publish.vue @@ -1,7 +1,10 @@ - + {{ t('调整分组上线') }} + + + {{ approveData.type === ONLINE_TYPE.Periodically ? t('等待定时上线') : t('确定上线') }} + @@ -69,6 +87,7 @@ :current-version-groups="groupsPendingtoPublish" @publish="handleOpenPublishDialog" @close="isDiffSliderShow = false" /> + diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/components/version-diff/index.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/components/version-diff/index.vue index 1624056eba..fc555b96fc 100644 --- a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/components/version-diff/index.vue +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/components/version-diff/index.vue @@ -59,14 +59,17 @@ - {{ t('上线版本') }} + {{ isApprovalMode ? t('通过') : t('上线版本') }} - {{ t('关闭') }} + + {{ t('驳回') }} + + {{ t('关闭') }} @@ -109,9 +112,11 @@ selectedConfig?: IConfigDiffSelected; // 默认选中的配置文件 versionDiffList?: IConfigVersion[]; selectedKvConfigId?: number; // 选中的kv类型配置id + isApprovalMode?: boolean; // 是否审批模式(操作记录-去审批-拒绝) + btnLoading?: boolean; }>(); - const emits = defineEmits(['update:show', 'publish']); + const emits = defineEmits(['update:show', 'publish', 'reject']); const route = useRoute(); const bkBizId = ref(String(route.params.spaceId)); diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/version-list-aside/version-simple-list.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/version-list-aside/version-simple-list.vue index 70361a4eb5..2d5518b704 100644 --- a/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/version-list-aside/version-simple-list.vue +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/detail/config/version-list-aside/version-simple-list.vue @@ -133,6 +133,8 @@ if (versionDetail) { versionData.value = versionDetail; refreshVersionListFlag.value = false; + // 默认选中新增的版本时,路由参数versionId需要更新 + router.push({ name: route.name as string, params: { versionId: versionDetail.id } }); } } }); diff --git a/bcs-services/bcs-bscp/ui/src/views/space/service/list/components/create-service.vue b/bcs-services/bcs-bscp/ui/src/views/space/service/list/components/create-service.vue index 3cfde6dbdb..48fd87b3e5 100644 --- a/bcs-services/bcs-bscp/ui/src/views/space/service/list/components/create-service.vue +++ b/bcs-services/bcs-bscp/ui/src/views/space/service/list/components/create-service.vue @@ -6,7 +6,11 @@ :before-close="handleBeforeClose" @closed="close"> - +
+ { this.selector.emptyText } +