This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 9dc56720b4ebcc66c6f9ed6944008330efaa2144 Author: Qian Xia <lauraxiaq...@gmail.com> AuthorDate: Wed Jul 5 14:41:52 2023 +0800 KYLIN-5594 data permission --- kystudio/package.json | 2 +- kystudio/src/components/admin/Diagnostic/index.vue | 5 +- kystudio/src/components/admin/Diagnostic/store.js | 26 +- .../User/UserDataPermission/UserDataPermission.vue | 96 ++ .../admin/User/UserDataPermission/locales.js | 7 + .../admin/User/UserDataPermission/store.js | 45 + kystudio/src/components/admin/User/index.vue | 69 +- kystudio/src/components/admin/User/locales.js | 3 +- .../EditExcludeColumnsDialog.vue | 4 +- .../src/components/project/project_authority.vue | 66 +- kystudio/src/components/project/user_access.vue | 1228 ++++++++++++++++++++ kystudio/src/router/routerGuard.js | 5 +- kystudio/src/service/project.js | 3 + kystudio/src/service/system.js | 4 +- kystudio/src/service/user.js | 8 +- kystudio/src/store/config.js | 3 +- kystudio/src/store/project.js | 3 + kystudio/src/store/types.js | 3 + kystudio/src/store/user.js | 15 +- 19 files changed, 1545 insertions(+), 50 deletions(-) diff --git a/kystudio/package.json b/kystudio/package.json index 952b2aa7b2..92b45374a8 100644 --- a/kystudio/package.json +++ b/kystudio/package.json @@ -27,7 +27,7 @@ "js-beautify": "1.6.14", "jsplumb": "2.6.9", "konva": "4.2.2", - "kyligence-kylin-ui": "5.0.3", + "kyligence-kylin-ui": "5.0.4", "less": "2.7.2", "less-loader": "2.2.3", "moment-timezone": "0.5.14", diff --git a/kystudio/src/components/admin/Diagnostic/index.vue b/kystudio/src/components/admin/Diagnostic/index.vue index 7f3dbd3468..c79454ffbe 100644 --- a/kystudio/src/components/admin/Diagnostic/index.vue +++ b/kystudio/src/components/admin/Diagnostic/index.vue @@ -393,7 +393,8 @@ export default class Diagnostic extends Vue { let data = {} if (this.isJobDiagnosis) { data = { - job_id: this.jobId + job_id: this.jobId, + project: this.currentSelectedProject } } else if (this.isQueryHistory) { data = { @@ -440,7 +441,7 @@ export default class Diagnostic extends Vue { retryJob (item) { const { host, start, end, id } = item this.delDumpid(id) - this.getDumpRemote({ host, start, end, job_id: this.jobId || '', tm: this.getTimes() }) + this.getDumpRemote({ host, start, end, job_id: this.jobId || '', project: this.currentSelectedProject, tm: this.getTimes() }) } changeCheckAllType (val) { this.indeterminate = false diff --git a/kystudio/src/components/admin/Diagnostic/store.js b/kystudio/src/components/admin/Diagnostic/store.js index edfacda84f..ad5dacd409 100644 --- a/kystudio/src/components/admin/Diagnostic/store.js +++ b/kystudio/src/components/admin/Diagnostic/store.js @@ -125,7 +125,7 @@ export default { if (state.isReset) return const { data } = res.data await commit(types.UPDATE_DUMP_IDS, { host, start, end, id: data, tm }) - dispatch(types.POLLING_STATUS_MSG, { host, id: data }) + dispatch(types.POLLING_STATUS_MSG, { host, id: data, project }) resolve(data) }).catch((err) => { handleError(err) @@ -134,10 +134,10 @@ export default { }) }, // 生成诊断包 - [types.GET_DUMP_REMOTE] ({ state, commit, dispatch }, { host = '', start = '', end = '', job_id = '', tm }) { + [types.GET_DUMP_REMOTE] ({ state, commit, dispatch }, { host = '', start = '', end = '', job_id = '', project, tm }) { if (!host) return return new Promise((resolve, reject) => { - api.system.getDumpRemote({ host, start, end, job_id }).then(async (res) => { + api.system.getDumpRemote({ host, start, end, job_id, project }).then(async (res) => { if (state.isReset) return const { data } = res.data await commit(types.UPDATE_DUMP_IDS, { host, start, end, id: data, tm }) @@ -167,9 +167,9 @@ export default { }) }, // 获取诊断报生成进度 - [types.GET_STATUS_REMOTE] ({ commit }, { host, id }) { + [types.GET_STATUS_REMOTE] ({ commit }, { host, id, project }) { return new Promise((resolve, reject) => { - api.system.getStatusRemote({ host, id }).then(res => { + api.system.getStatusRemote({ host, id, project }).then(res => { const { data } = res.data commit(types.SET_DUMP_PROGRESS, {...data, id}) resolve(res) @@ -179,16 +179,16 @@ export default { }) }, // 轮询接口获取信息 - [types.POLLING_STATUS_MSG] ({ state, commit, dispatch }, { host, id }) { + [types.POLLING_STATUS_MSG] ({ state, commit, dispatch }, { host, id, project }) { if (state.isReset) return - dispatch(types.GET_STATUS_REMOTE, { host, id }).then((res) => { + dispatch(types.GET_STATUS_REMOTE, { host, id, project }).then((res) => { timer[id] = setTimeout(() => { - dispatch(types.POLLING_STATUS_MSG, { host, id }) + dispatch(types.POLLING_STATUS_MSG, { host, id, project }) }, pollingTime) const { data } = res.data if (data.status === '000' && data.stage === 'DONE') { clearTimeout(timer[id]) - dispatch(types.DOWNLOAD_DUMP_DIAG, {host, id}) + dispatch(types.DOWNLOAD_DUMP_DIAG, {host, id, project}) } else if (['001', '002', '999'].includes(data.status)) { clearTimeout(timer[id]) } @@ -199,11 +199,15 @@ export default { }) }, // 下载诊断包 - [types.DOWNLOAD_DUMP_DIAG] (_, { host, id }) { + [types.DOWNLOAD_DUMP_DIAG] (_, { host, id, project }) { let dom = document.createElement('a') dom.download = true // 兼容IE 10以下 无origin属性问题,此处用protocol和host拼接 - dom.href = `${location.protocol}//${location.host}${apiUrl}system/diag?host=${host}&id=${id}` + let href = `${location.protocol}//${location.host}${apiUrl}system/diag?host=${host}&id=${id}` + if (project) { + href = href + `&project=${project}` + } + dom.href = href document.body.appendChild(dom) dom.click() document.body.removeChild(dom) diff --git a/kystudio/src/components/admin/User/UserDataPermission/UserDataPermission.vue b/kystudio/src/components/admin/User/UserDataPermission/UserDataPermission.vue new file mode 100644 index 0000000000..860fd0e66f --- /dev/null +++ b/kystudio/src/components/admin/User/UserDataPermission/UserDataPermission.vue @@ -0,0 +1,96 @@ +<template> + <el-dialog class="user-data-permission" width="400px" + :title="$t('dataPermission')" + :visible="isShow" + :close-on-press-escape="false" + :close-on-click-modal="false" + @close="isShow && closeHandler()"> + <span>{{$t('kylinLang.common.userName')}}</span> + <el-input class="ksd-mt-8" v-model="userName" :disabled="true"></el-input> + <div class="ksd-mt-8 flex"> + <span>{{$t('dataPermission')}}</span> + <el-tooltip class="item" effect="dark" :content="$t('dataPermissionTips')" placement="bottom"> + <i class="el-icon-ksd-info ksd-fs-14 ksd-ml-5"></i> + </el-tooltip> + <el-switch + :value="dataPermission" + @change="handleChangePermission" + class="ksd-ml-8" + :active-text="$t('kylinLang.common.OFF')" + :inactive-text="$t('kylinLang.common.ON')"> + </el-switch> + </div> + <div slot="footer" class="dialog-footer ky-no-br-space"> + <el-button size="medium" @click="closeHandler()">{{$t('kylinLang.common.cancel')}}</el-button> + <el-button type="primary" size="medium" @click="submit" :loading="isLoading">{{$t('kylinLang.common.submit')}}</el-button> + </div> + </el-dialog> + </template> + + <script> + import Vue from 'vue' + import { Component } from 'vue-property-decorator' + import { mapActions, mapMutations, mapState } from 'vuex' + import vuex from '../../../../store' + import locales from './locales' + import store, { types } from './store' + import { handleError } from 'util/business' + vuex.registerModule(['modals', 'UserDataPermission'], store) + @Component({ + computed: { + // Store数据注入 + ...mapState('UserDataPermission', { + isShow: state => state.isShow, + dataPermission: state => state.dataPermission, + userName: state => state.userName, + callback: state => state.callback + }) + }, + methods: { + // Store方法注入 + ...mapMutations('UserDataPermission', { + setModal: types.SET_MODAL, + hideModal: types.HIDE_MODAL + }), + // 后台接口请求 + ...mapActions({ + updataUserDataPermission: 'UPDATE_USER_DATA_PERMISSION' + }) + }, + locales + }) + export default class UserDataPermission extends Vue { + isLoading = false + closeHandler () { + this.hideModal() + } + handleChangePermission (val) { + this.setModal({ dataPermission: val }) + } + async submit () { + this.isLoading = true + try { + await this.updataUserDataPermission({ username: this.userName, enabled: this.dataPermission }) + this.isLoading = false + this.callback(true) + } catch (e) { + handleError(e) + this.isLoading = false + } + this.hideModal() + } + } + </script> + <style lang="less"> + @import '../../../../assets/styles/variables.less'; + .user-data-permission { + .flex { + display: flex; + align-items: center; + } + .el-icon-ksd-info { + color: @text-placeholder-color; + } + } + </style> + \ No newline at end of file diff --git a/kystudio/src/components/admin/User/UserDataPermission/locales.js b/kystudio/src/components/admin/User/UserDataPermission/locales.js new file mode 100644 index 0000000000..8babfe6739 --- /dev/null +++ b/kystudio/src/components/admin/User/UserDataPermission/locales.js @@ -0,0 +1,7 @@ + +export default { + 'en': { + dataPermission: 'data Permission', + dataPermissionTips: 'Allow users to access data, including viewing sample data and querying with SQL' + } +} diff --git a/kystudio/src/components/admin/User/UserDataPermission/store.js b/kystudio/src/components/admin/User/UserDataPermission/store.js new file mode 100644 index 0000000000..331f9c1022 --- /dev/null +++ b/kystudio/src/components/admin/User/UserDataPermission/store.js @@ -0,0 +1,45 @@ +const types = { + SHOW_MODAL: 'SHOW_MODAL', + HIDE_MODAL: 'HIDE_MODAL', + SET_MODAL: 'SET_MODAL', + CALL_MODAL: 'CALL_MODAL' +} +// 声明:初始state状态 +const initialState = JSON.stringify({ + isShow: false, + callback: null, + dataPermission: false, + userName: '' +}) + +export default { + // state深拷贝 + state: JSON.parse(initialState), + mutations: { + // 显示Modal弹窗 + [types.SHOW_MODAL]: (state) => { + state.isShow = true + }, + // 隐藏Modal弹窗 + [types.HIDE_MODAL]: (state) => { + state.isShow = false + }, + // 设置Modal中的值 + [types.SET_MODAL]: (state, payload) => { + for (const key in payload) { + state[key] = payload[key] + } + } + }, + actions: { + [types.CALL_MODAL] ({ commit }, { dataPermission, userName }) { + return new Promise(resolve => { + commit(types.SET_MODAL, { dataPermission, userName, callback: resolve }) + commit(types.SHOW_MODAL) + }) + } + }, + namespaced: true +} + +export { types } diff --git a/kystudio/src/components/admin/User/index.vue b/kystudio/src/components/admin/User/index.vue index e4f926de97..79b0816a5f 100644 --- a/kystudio/src/components/admin/User/index.vue +++ b/kystudio/src/components/admin/User/index.vue @@ -14,8 +14,7 @@ type="primary" size="medium" icon="el-ksd-icon-add_22" - v-if="userActions.includes('addUser')" - :disabled="!isTestingSecurityProfile" + v-if="userActions.includes('addUser')&&isTestingSecurityProfile" @click="editUser('new')"> {{$t('user')}} </el-button> @@ -55,6 +54,12 @@ </common-tip> </template> </el-table-column> + <!-- 表:是否有数据权限 --> + <el-table-column :label="$t('dataPermission')" align="center" :width="120"> + <template slot-scope="scope"> + <i class="el-icon-ksd-good_health admin-svg" v-if="scope.row.hasQueryPermission"></i> + </template> + </el-table-column> <!-- 表:是否系统管理员列 --> <el-table-column :label="$t('admin')" align="center" :width="120"> <template slot-scope="scope"> @@ -69,26 +74,34 @@ </template> </el-table-column> <!-- 表:action列 --> - <el-table-column v-if="isActionShow" :label="$t('action')" :width="87"> + <el-table-column v-if="isActionShow&&isTestingSecurityProfile" :label="$t('action')" :width="87"> <template slot-scope="scope"> <el-tooltip :content="$t('resetPassword')" effect="dark" placement="top"> - <i class="el-icon-ksd-table_reset_password ksd-fs-14 ksd-mr-10" :class="{'is-disabled': !isTestingSecurityProfile}" v-if="userActions.includes('changePassword') || scope.row.uuid === currentUser.uuid" @click="editUser(scope.row.uuid === currentUser.uuid ? 'password' : 'resetUserPassword', scope.row)"></i> + <i class="el-icon-ksd-table_reset_password ksd-fs-14 ksd-mr-10" v-if="userActions.includes('changePassword') || scope.row.uuid === currentUser.uuid" @click="editUser(scope.row.uuid === currentUser.uuid ? 'password' : 'resetUserPassword', scope.row)"></i> </el-tooltip><span> </span><el-tooltip :content="$t('groupMembership')" effect="dark" placement="top"> - <i class="el-icon-ksd-table_group ksd-fs-14 ksd-mr-10" :class="{'is-disabled': !isTestingSecurityProfile}" v-if="userActions.includes('assignGroup')" @click="editUser('group', scope.row)"></i> + <i class="el-icon-ksd-table_group ksd-fs-14 ksd-mr-10" v-if="userActions.includes('assignGroup')" @click="editUser('group', scope.row)"></i> </el-tooltip><span> </span><common-tip :content="$t('kylinLang.common.moreActions')" v-if="isMoreActionShow"><el-dropdown trigger="click"> - <i class="el-icon-ksd-table_others" :class="{'is-disabled': !isTestingSecurityProfile}"></i> + <i class="el-icon-ksd-table_others"></i> <el-dropdown-menu slot="dropdown"> - <el-dropdown-item :disabled="!isTestingSecurityProfile" v-if="userActions.includes('editUser')&&scope.row.uuid !== currentUser.uuid" @click.native="editUser('edit', scope.row)">{{$t('editRole')}}</el-dropdown-item> - <el-dropdown-item :disabled="!isTestingSecurityProfile" v-if="userActions.includes('deleteUser')" @click.native="dropUser(scope.row)">{{$t('drop')}}</el-dropdown-item> - <el-dropdown-item :disabled="!isTestingSecurityProfile" v-if="userActions.includes('disableUser') && scope.row.disabled" @click.native="changeStatus(scope.row)">{{$t('enable')}}</el-dropdown-item> - <el-dropdown-item :disabled="!isTestingSecurityProfile" v-if="userActions.includes('disableUser') && !scope.row.disabled" @click.native="changeStatus(scope.row)">{{$t('disable')}}</el-dropdown-item> + <el-dropdown-item v-if="userActions.includes('editUser')&&scope.row.uuid !== currentUser.uuid" @click.native="editUser('edit', scope.row)">{{$t('editRole')}}</el-dropdown-item> + <el-dropdown-item :disabled="!hasQueryPermission" v-if="userActions.includes('editUserDataPermission')&&scope.row.uuid !== currentUser.uuid&&scope.row.admin&&!scope.row.isSuperAdmin" @click.native="editUserDataPermission(scope.row)">{{$t('dataPermission')}}</el-dropdown-item> + <el-dropdown-item v-if="userActions.includes('deleteUser')" @click.native="dropUser(scope.row)">{{$t('drop')}}</el-dropdown-item> + <el-dropdown-item v-if="userActions.includes('disableUser') && scope.row.disabled" @click.native="changeStatus(scope.row)">{{$t('enable')}}</el-dropdown-item> + <el-dropdown-item v-if="userActions.includes('disableUser') && !scope.row.disabled" @click.native="changeStatus(scope.row)">{{$t('disable')}}</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </common-tip> </template> </el-table-column> + <el-table-column v-if="isActionShow&&!isTestingSecurityProfile" :label="$t('action')" :width="87"> + <template slot-scope="scope"> + <el-tooltip :content="$t('dataPermission')" effect="dark" placement="top"> + <i class="el-ksd-n-icon-acl-data-outlined ksd-fs-14 ksd-mr-10" v-if="userActions.includes('editUserDataPermission')&&scope.row.uuid !== currentUser.uuid&&scope.row.admin&&!scope.row.isSuperAdmin" @click="editUserDataPermission(scope.row)"></i> + </el-tooltip> + </template> + </el-table-column> </el-table> <kylin-pager @@ -99,6 +112,7 @@ :curPage="pagination.page_offset+1" @handleCurrentChange="handleCurrentChange"> </kylin-pager> + <UserDataPermission/> </div> </template> @@ -109,7 +123,8 @@ import { Component } from 'vue-property-decorator' import locales from './locales' import { pageRefTags, bigPageCount } from 'config' -import { handleError, kylinConfirm } from '../../../util' +import { handleError, handleSuccessAsync, kylinConfirm } from '../../../util' +import UserDataPermission from './UserDataPermission/UserDataPermission' @Component({ computed: { @@ -127,12 +142,19 @@ import { handleError, kylinConfirm } from '../../../util' removeUser: 'REMOVE_USER', loadUsersList: 'LOAD_USERS_LIST', loadUserListByGroupName: 'GET_USERS_BY_GROUPNAME', - updateStatus: 'UPDATE_STATUS' + updateStatus: 'UPDATE_STATUS', + getCurrentUserDataPermission: 'GET_CURRENT_USER_DATA_PERMISSION' }), ...mapActions('UserEditModal', { callUserEditModal: 'CALL_MODAL' + }), + ...mapActions('UserDataPermission', { + callDataPermission: 'CALL_MODAL' }) }, + components: { + UserDataPermission + }, locales, beforeRouteEnter: (to, from, next) => { if (from.name === 'GroupDetail') { @@ -157,6 +179,7 @@ export default class SecurityUser extends Vue { page_offset: 0 } isLoadingUsers = false + hasQueryPermission = false get currentGroup () { const current = this.$store.state.user.usersGroupList.filter((g) => { return g.group_name === this.$route.params.groupName @@ -178,6 +201,8 @@ export default class SecurityUser extends Vue { analyst: user.authorities.some(role => role.authority === 'ROLE_ANALYST'), default_password: user.default_password, authorities: user.authorities, + hasQueryPermission: user.has_query_permission, + isSuperAdmin: user.is_super_admin, groups: user.authorities.map(role => role.authority), uuid: user.uuid })) @@ -232,6 +257,13 @@ export default class SecurityUser extends Vue { isSubmit && this.loadUsers(this.filterName) } + async editUserDataPermission (row) { + const isSubmit = await this.callDataPermission({ dataPermission: row.hasQueryPermission, userName: row.username }) + if (isSubmit) { + this.loadUsers() + } + } + async dropUser (userDetail) { if (!this.isTestingSecurityProfile) return try { @@ -263,8 +295,15 @@ export default class SecurityUser extends Vue { } } - mounted () { + async mounted () { this.loadUsers() + try { + const res = await this.getCurrentUserDataPermission({ username: this.currentUser.username }) + const { enabled } = await handleSuccessAsync(res) + this.hasQueryPermission = enabled + } catch (e) { + handleError(e) + } } } </script> @@ -279,6 +318,10 @@ export default class SecurityUser extends Vue { cursor: default; } .user-table { + i { + cursor: pointer; + } + .el-ksd-n-icon-acl-data-outlined:hover, .el-icon-ksd-table_reset_password:hover, .el-icon-ksd-table_group:hover, .el-icon-ksd-table_others:hover { diff --git a/kystudio/src/components/admin/User/locales.js b/kystudio/src/components/admin/User/locales.js index 978bf09f07..ee8282de7f 100644 --- a/kystudio/src/components/admin/User/locales.js +++ b/kystudio/src/components/admin/User/locales.js @@ -16,6 +16,7 @@ export default { cofirmDelUser: 'Are you sure you want to delete the user {userName} ?', delUserTitle: 'Delete User', userList: 'User List', - changeUserTips: 'Are you sure you want to {status} the user {userName} ?' + changeUserTips: 'Are you sure you want to {status} the user {userName} ?', + dataPermission: 'Data Permission' } } diff --git a/kystudio/src/components/common/EditExcludeColumnsDialog/EditExcludeColumnsDialog.vue b/kystudio/src/components/common/EditExcludeColumnsDialog/EditExcludeColumnsDialog.vue index 7f96397026..188a7675ae 100644 --- a/kystudio/src/components/common/EditExcludeColumnsDialog/EditExcludeColumnsDialog.vue +++ b/kystudio/src/components/common/EditExcludeColumnsDialog/EditExcludeColumnsDialog.vue @@ -75,14 +75,14 @@ </template> </el-table-column> </el-table> - <kap-pager + <kylin-pager class="ksd-center ksd-mt-16" ref="columnPager" :refTag="pageRefTags.exclusionColumnsPager" :totalSize="columnsTotalSize" :curPage="pagination.page_offset + 1" :perPageSize="pagination.page_size" @handleCurrentChange="handleCurrentChange"> - </kap-pager> + </kylin-pager> <div slot="footer" class="dialog-footer"> <el-popover ref="popover" diff --git a/kystudio/src/components/project/project_authority.vue b/kystudio/src/components/project/project_authority.vue index 2f8a10e464..7978a3179e 100644 --- a/kystudio/src/components/project/project_authority.vue +++ b/kystudio/src/components/project/project_authority.vue @@ -29,7 +29,12 @@ </el-col> </el-row> <div> - <el-table :data="userAccessList" :empty-text="emptyText" class="user-access-table" key="user"> + <el-table ref="userAccessTable" @expand-change="expandChange" :data="userAccessList" :empty-text="emptyText" class="user-access-table" key="user"> + <el-table-column type="expand" :width="36"> + <template slot-scope="props"> + <user_access @reload="loadAccess" :projectName="currentProject" :row="props.row"></user_access> + </template> + </el-table-column> <el-table-column :label="$t('userOrGroup')" prop="role_or_name" class-name="role-name-cell" show-overflow-tooltip> <template slot-scope="props"> <i :class="{'el-icon-ksd-table_admin': props.row.type === 'User', 'el-icon-ksd-table_group': props.row.type === 'Group'}"></i> @@ -66,9 +71,9 @@ <el-dialog :title="authorTitle" width="960px" class="user-access-dialog" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="authorizationVisible" @close="initAccessData"> <div class="content-container"> <div class="author-tips"> - <div class="item-point">{{$t('authorTips')}}</div> - <div class="item-point">{{$t('authorTips1')}}</div> <div class="item-point" v-html="$t('authorTips2')"></div> + <div class="item-point ksd-mt-16">{{$t('authorTips')}}</div> + <div class="item-point">{{$t('authorTips1')}}</div> </div> <div class="ksd-title-label-small">{{$t('selectUserAccess')}}</div> <div v-for="(accessMeta, index) in accessMetas" :key="index" class="user-group-select ksd-mt-10 ky-no-br-space"> @@ -138,10 +143,11 @@ <script> import Vue from 'vue' import { Component } from 'vue-property-decorator' -import { objectClone } from '../../util' +import { objectClone, indexOfObjWithSomeKey } from '../../util' import { handleSuccess, handleError, kylinConfirm, hasRole, hasPermissionOfProjectAccess } from '../../util/business' import { mapActions, mapGetters } from 'vuex' import { permissions, pageRefTags, pageCount } from 'config' +import userAccess from './user_access' @Component({ methods: { ...mapActions({ @@ -162,6 +168,9 @@ import { permissions, pageRefTags, pageCount } from 'config' 'projectActions' ]) }, + components: { + 'user_access': userAccess + }, locales: { 'en': { projectTitle: 'Authorization (Project: {projectName})', @@ -184,7 +193,7 @@ import { permissions, pageRefTags, pageCount } from 'config' Group: 'User Group', User: 'User', Query: 'Query', - Admin: 'Admin', + Admin: 'Project Admin', Management: 'Management', Operation: 'Operation', tableName: 'Table Name', @@ -193,14 +202,13 @@ import { permissions, pageRefTags, pageCount } from 'config' deleteAccessTip: 'Are you sure you want to delete the authorization of "{userName}" in this project?', access: 'Role', deleteAccessTitle: 'Delete Authorization', - authorTips: 'Can\'t add system admin to the list, as this role already has full access to all projects.', + authorTips: 'Can\'t add System Admin to the list, as this role already has full access to all projects.', authorTips1: 'By default, the added user/user group would be granted full access to all the tables in the project.', - authorTips2: `What roles does Kylin provide?<br> - The relationship of each role is: Admin > Management > Operation > Query. For example, Admin includes all the permissions of the other three roles. Management includes all the permissions of Operation and Query. Operation includes all the permissions of Query.<br> - 1. Query: For business analyst who would need permissions to query tables or indexes.<br> - 2. Operation: For the operator who need permissions to build indexes and monitor job status.<br> - 3. Management: For the model designer who would need permissions to load tables and design models.<br> - 4. Admin: For the project admin who would need all permissions and could manage and maintain this project, including loading tables, authorizing user access permissions, etc.`, + authorTips2: `<div class="ksd-mb-8">What roles does Kyligence Enterprise provide?</div> + <p><span>Project Admin</span><span>For the project admin who needs all permission and could manage and maintain this project, including loading tables, authorizing user access permission, etc.</span></p> + <p><span>Management</span><span>For the model designer who needs permission to load tables, design models, build indexes and monitor job status.</span></p> + <p><span>Operation</span><span>For the operator who needs permission to build indexes and monitor job status.</span></p> + <p><span>Query</span><span>For the business analyst who needs permission to query tables or indexes.</span></p>`, noAuthorityTip: 'Access denied. Please try again after logging in.' } } @@ -223,6 +231,7 @@ export default class ProjectAuthority extends Vue { authorizationVisible = false authorForm = {name: [], editName: '', role: 'Admin'} isEditAuthor = false + expandedRows = [] showMask = { 1: 'Query', 16: 'Admin', @@ -316,6 +325,14 @@ export default class ProjectAuthority extends Vue { return flag } } + expandChange (row) { + const index = indexOfObjWithSomeKey(this.expandedRows, 'id', row.id) + if (index !== -1) { + this.expandedRows.splice(index, 1) + } else { + this.expandedRows.push(row) + } + } showLimitTips (val) { return val ? this.userTotalSize > 100 : this.groupTotalSize > 100 } @@ -500,6 +517,18 @@ export default class ProjectAuthority extends Vue { access.accessDetails = [] return access }) || [] + if (this.expandedRows.length && this.$refs.userAccessTable) { + const expandedRows = objectClone(this.expandedRows) + this.expandedRows = [] + expandedRows.forEach((item, i) => { + const index = indexOfObjWithSomeKey(this.userAccessList, 'id', item.id) + if (index !== -1) { + this.$nextTick(() => { + this.$refs.userAccessTable.toggleRowExpansion(this.userAccessList[index], true) + }) + } + }) + } }) }, (res) => { handleError(res) @@ -586,6 +615,19 @@ export default class ProjectAuthority extends Vue { top: 8px; left: -10px; } + p { + margin-left: -8px; + border-bottom: 1px solid @ke-color-secondary; + span { + padding: 8px; + display: table-cell; + &:first-child { + width: 130px; + box-sizing: border-box; + vertical-align: middle; + } + } + } } } .user-group-select { diff --git a/kystudio/src/components/project/user_access.vue b/kystudio/src/components/project/user_access.vue new file mode 100644 index 0000000000..69e2226a9a --- /dev/null +++ b/kystudio/src/components/project/user_access.vue @@ -0,0 +1,1228 @@ +<template> + <div class="user-access-block" v-loading="loading"> + <div class="data-permission clearfix"> + <div class="flex ksd-fleft"> + <span class="ksd-title-label">{{$t('dataPermission')}}</span> + <el-tooltip class="item" effect="dark" :content="$t('dataPermissionTips')" placement="bottom"> + <i class="el-icon-ksd-info ksd-fs-14 ksd-ml-5"></i> + </el-tooltip> + <el-switch + :value="ext_permissions" + class="ksd-ml-8" + @change="handleChangePermission" + :disabled="!isDataPermission" + :active-text="$t('kylinLang.common.OFF')" + :inactive-text="$t('kylinLang.common.ON')"> + </el-switch> + </div> + </div> + <el-button type="primary" plain size="small" icon="el-ksd-n-icon-edit-outlined" class="ksd-mt-10" @click="editAccess" v-if="!isEdit && isAuthority && ext_permissions">{{$t('editACL')}}</el-button> + <el-row class="ksd-mt-10" v-if="ext_permissions"> + <el-col :span="8"> + <div class="access-card"> + <div class="access-title"> + <span v-if="!isEdit">{{$t('accessTables')}} ({{tableAuthorizedNum}})</span> + <el-checkbox v-model="isAllTablesAccess" @change="checkAllTables" :indeterminate="tableAuthorizedNum !== totalNum && tableAuthorizedNum>0" :disabled="!tables.length" v-else>{{$t('accessTables')}} ({{tableAuthorizedNum}}/{{totalNum}})</el-checkbox> + </div> + <div class="access-search"> + <el-input size="mini" :placeholder="$t('searchKey')" v-model="tableFilter"> + <i slot="prefix" class="el-input__icon el-ksd-icon-search_16"></i> + </el-input> + </div> + <div class="access-tips" v-if="isAllTablesAccess&&!isEdit"> + <i class="el-icon-ksd-info ksd-fs-14"></i> + <span class="ksd-fs-12">{{$t('accessTips')}}</span> + </div> + <div class="access-content tree-content" :class="{'all-tips': isAllTablesAccess&&!isEdit}"> + <el-tree + v-if="filterTableData.length&&isRerender" + show-overflow-tooltip + node-key="id" + ref="tableTree" + class="acl-tree" + :data="filterTableData" + :show-checkbox="isEdit" + :props="defaultProps" + :render-after-expand="false" + :highlight-current="true" + :default-expanded-keys="defaultExpandedKeys" + :default-checked-keys="defaultCheckedKeys" + @check="checkChange" + @node-expand="pushTableId" + @node-collapse="removeTableId" + @node-click="handleNodeClick"> + <span class="custom-tree-node" slot-scope="{ node, data }"> + <i class="ksd-mr-2" :class="data.icon"></i> + <span class="ky-ellipsis" :class="data.class" :title="node.label">{{ node.label }}</span> + </span> + </el-tree> + <kylin-nodata :content="emptyText" v-else> + </kylin-nodata> + </div> + </div> + </el-col> + <el-col :span="8"> + <div class="access-card column-card"> + <div class="access-title"> + <span v-if="!isEdit">{{$t('accessColumns')}} ({{colAuthorizedNum}})</span> + <el-checkbox v-model="selectAllColumns" @change="checkAllColumns" :disabled="!isCurrentTableChecked || !columns.length" :indeterminate="colAuthorizedNum !== columns.length && colAuthorizedNum>0" v-else>{{$t('accessColumns')}} ({{colAuthorizedNum}}/{{columns.length}})</el-checkbox> + </div> + <div class="access-search"> + <el-input size="mini" :placeholder="$t('searchKey')" v-model="columnFilter"> + <i slot="prefix" class="el-input__icon el-ksd-icon-search_16"></i> + </el-input> + </div> + <div class="access-tips" v-if="isAllColAccess&&!isEdit"> + <i class="el-icon-ksd-info ksd-fs-14"></i> + <span class="ksd-fs-12">{{$t('accessColsTips')}}</span> + </div> + <div class="access-content" :class="{'all-tips': isAllColAccess&&!isEdit}"> + <div v-if="pagedFilterColumns.length"> + <ul> + <li v-for="col in pagedFilterColumns" :key="col.name"> + <el-checkbox @change="val => selectColumn(val, col.name)" :disabled="!isCurrentTableChecked" size="medium" v-if="isEdit" :value="col.authorized">{{col.name}}</el-checkbox> + <span v-else>{{col.name}}</span> + </li> + </ul> + <div class="list-load-more" @click="loadMoreCols" v-if="pagedFilterColumns.length<filterCols.length">{{$t('loadMore')}}</div> + </div> + <kylin-nodata :content="emptyText2" v-else> + </kylin-nodata> + </div> + </div> + </el-col> + <el-col :span="8"> + <div class="access-card row-card"> + <div class="access-title"> + <span>{{$t('accessRows')}}</span> + <!-- <el-button type="primary" plain size="small" icon="el-ksd-icon-add_16" class="ksd-fright ksd-mt-5" @click="addRowAccess" v-if="isEdit" :disabled="!isCurrentTableChecked">{{$t('addRowAccess')}}</el-button> --> + </div> + <div class="access-search"> + <el-input size="mini" :placeholder="$t('searchKey')" v-model="rowSearch"> + <i slot="prefix" class="el-input__icon el-ksd-icon-search_16"></i> + </el-input> + </div> + <div class="access-tips" v-if="isCurrentTableChecked&&row_filter&&!row_filter.filter_groups.length"> + <i class="el-icon-ksd-info ksd-fs-14"></i> + <span class="ksd-fs-12">{{$t('accessRowsTips')}}</span> + </div> + <div class="access-content"> + <el-select class="ksd-mt-10 ksd-mb-5 ksd-ml-10 join-type" size="small" v-model="row_filter.type" v-if="isEdit && row_filter && getSearchFilterdGroup(row_filter.filter_groups).length" @change="changeRowFilterType"> + <el-option label="AND" value="AND"></el-option> + <el-option label="OR" value="OR"></el-option> + </el-select> + <div v-for="(fg, fgIndex) in getSearchFilterdGroup(row_filter.filter_groups)" :key="fgIndex"> + <div class="filter-groups" :class="{'is-group': fg.is_group}"> + <div class="filter-group-block"> + <div class="clearfix"> + <el-select class="join-type ksd-fleft" size="small" v-model="fg.type" @change="changeFilterGroupTpye(fgIndex)" v-if="fg.is_group&&fg.filters.length&&isEdit"> + <el-option label="AND" value="AND"></el-option> + <el-option label="OR" value="OR"></el-option> + </el-select> + </div> + <el-dropdown class="group-action-btn" size="small" v-if="fg.is_group&&isEdit"> + <span class="el-dropdown-link"> + <i class="el-icon-ksd-table_others"></i> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item @click.native="addRowAccess(fgIndex)"> + <i class="el-ksd-icon-add_16"></i> + {{$t('filters')}} + </el-dropdown-item> + <el-dropdown-item @click.native="deleteFG(fgIndex)"> + <i class="el-icon-ksd-table_delete"></i> + {{$t('kylinLang.common.delete')}} + </el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + <!-- getFilterFilters 搜索后的filters, getlimitFilters 是搜索后length 大于3时要收拢filterGroup --> + <ul v-if="getFilterFilters(fg.filters).length" :key="fg.isExpand"> + <div v-for="(row, key) in getlimitFilters(fg)" :key="key" > + <li class="row-list"> + <el-row> + <el-col :span="isEdit ? 23 : 24"> + <span>{{row.column_name}}</span><span v-if="row.in_items.length"> IN </span><span v-if="row.in_items.length" class="row-values">({{row.in_items.toString()}})</span><span v-if="row.like_items.length"> LIKE </span><span class="row-values" v-if="row.like_items.length">({{row.like_items.toString()}})</span> + </el-col> + <el-col :span="1" class="ky-no-br-space btn-icons" v-if="isEdit"> + <!-- <i class="el-icon-ksd-table_edit ksd-fs-16" @click="editRowAccess(key, row)"></i> + <i class="el-icon-ksd-table_delete ksd-fs-16 ksd-ml-10" @click="deleteRowAccess(key, row)"></i> --> + <el-dropdown size="small" v-if="isEdit"> + <span class="el-dropdown-link"> + <i class="el-icon-ksd-table_others"></i> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item @click.native="editRowAccess(fgIndex, key, row)">{{$t('kylinLang.common.edit')}}</el-dropdown-item> + <el-dropdown-item @click.native="deleteRowAccess(fgIndex, key, row)">{{$t('kylinLang.common.delete')}}</el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + </el-col> + </el-row> + </li> + <div v-if="key !== getlimitFilters(fg).length - 1" class="join-type-label">{{fg.type}}</div> + </div> + </ul> + <div class="center ksd-mt-10" v-if="getFilterFilters(fg.filters).length > 3"> + <el-button type="primary" size="small" @click="toggleExpandFG(fg)" text> + {{fg.isExpand ? $t('collapse') : $t('expandAll')}} {{fg.isExpand ? '' : `(${fg.filters.length})`}} + </el-button> + </div> + <el-button type="primary" size="small" v-if="fg.is_group&&isEdit" icon="el-ksd-icon-add_16" @click="addRowAccess(fgIndex)" text>{{$t('filters')}}</el-button> + </div> + </div> + <div v-if="fgIndex !== getSearchFilterdGroup(row_filter.filter_groups).length - 1" class="join-type-label">{{row_filter.type}}</div> + </div> + <el-dropdown size="small" class="ksd-ml-10 ksd-mb-10" v-if="isEdit&&row_filter&&getSearchFilterdGroup(row_filter.filter_groups).length"> + <span class="el-dropdown-link"> + <el-button type="primary" size="small" :disabled="!isCurrentTableChecked" icon="el-ksd-icon-add_16" text>{{$t('add')}}</el-button> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item @click.native="addRowAccess(-1)">{{$t('filters')}}</el-dropdown-item> + <el-dropdown-item @click.native="addFilterGroups">{{$t('filterGroups')}}</el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + <kylin-nodata :content="emptyText3" v-if="isCurrentTableChecked&&row_filter&&!getSearchFilterdGroup(row_filter.filter_groups).length&&row_filter.filter_groups.length"> + </kylin-nodata> + <div class="view-all-tips" v-if="isCurrentTableChecked&&row_filter&&!row_filter.filter_groups.length"> + <div><i class="point">•</i> {{$t('viewAllDataTips')}}</div> + <div><i class="point">•</i> {{$t('viewAllDataTips1')}}</div> + <div class="add-rows-btns"> + <el-dropdown size="small" v-if="isEdit"> + <span class="el-dropdown-link"> + <el-button type="primary" size="small" :disabled="!isCurrentTableChecked" icon="el-ksd-icon-add_16" text>{{$t('add')}}</el-button> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item @click.native="addRowAccess(-1)">{{$t('filters')}}</el-dropdown-item> + <el-dropdown-item @click.native="addFilterGroups">{{$t('filterGroups')}}</el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + </div> + </div> + </div> + </div> + </el-col> + </el-row> + <div class="expand-footer ky-no-br-space ksd-right" v-if="isEdit"> + <el-button plain size="small" @click="cancelAccess">{{$t('kylinLang.common.cancel')}}</el-button> + <el-button type="primary" :disabled="disabledSubmitBtn" size="small" class="ksd-ml-10" :loading="submitLoading" @click="submitAccess">{{$t('kylinLang.common.submit')}}</el-button> + </div> + <el-dialog :title="rowAuthorTitle" width="960px" class="author_dialog" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="rowAccessVisible" @close="resetRowAccess"> + <div v-if="filterTotalLength+newFiltersLenth>maxFilterAndFilterValues || overedRowValueFilters.length>0"> + <el-alert class="ksd-mb-10" type="error" show-icon :closable="false"> + <span slot="title">{{$t('overFilterMaxTips')}} + <a class="a-like" @click="showErrorDetails = !showErrorDetails">{{$t('details')}} + <i :class="[showErrorDetails ? 'el-icon-ksd-more_01-copy' : 'el-icon-ksd-more_02', 'arrow']"></i> + </a> + </span> + </el-alert> + <div class="ksd-mtb-10 detail-content" v-if="showErrorDetails"> + <div v-if="filterTotalLength+newFiltersLenth>maxFilterAndFilterValues">{{$t('filterTotal')}}{{filterTotalLength+newFiltersLenth}}/{{maxFilterAndFilterValues}}</div> + <div v-if="overedRowValueFilters.length>0"> + {{$t('filterValuesTotal')}}{{overedRowValueFilters.toString()}} + </div> + </div> + </div> + <div class="like-tips-block ksd-mb-10"> + <div class="ksd-mb-5">{{$t('tipsTitle')}}<span class="review-details" @click="showDetails = !showDetails">{{$t('viewDetail')}}<i :class="[showDetails ? 'el-icon-ksd-more_01-copy' : 'el-icon-ksd-more_02', 'arrow']"></i></span></div> + <div class="detail-content" v-if="showDetails"> + <p>{{$t('rules1')}}</p> + <p>{{$t('rules2')}}</p> + <p>{{$t('rules3')}}</p> + </div> + </div> + <div v-for="(row, key) in rowLists" :key="key" class="ksd-mb-10"> + <el-select v-model="row.column_name" class="row-column" :placeholder="$t('kylinLang.common.pleaseSelectOrSearch')" filterable :disabled="isRowAuthorEdit" @change="isUnCharColumn(row.column_name, key)"> + <i slot="prefix" class="el-input__icon el-icon-search" v-if="!row.column_name"></i> + <el-option v-for="c in checkedColumns" :disabled="c.datatype.indexOf('char') === -1 && c.datatype.indexOf('varchar') === -1 && row.joinType === 'LIKE'" :key="c.name" :label="c.name" :value="c.name"> + <el-tooltip :content="c.name" effect="dark" placement="top"><span>{{c.name | omit(30, '...')}}</span></el-tooltip> + <span class="ky-option-sub-info">{{c.datatype.toLocaleLowerCase()}}</span> + </el-option> + </el-select> + <el-select + :placeholder="$t('kylinLang.common.pleaseSelect')" + style="width:75px;" + class="link-type" + popper-class="js_like-type" + :disabled="isRowAuthorEdit" + v-model="row.joinType"> + <el-option :disabled="row.isNeedDisableLike && key === 'LIKE'" :value="key" v-for="(key, i) in linkKind" :key="i">{{key}}</el-option> + </el-select> + <el-select + v-model="row.items" + multiple + filterable + clearable + remote + allow-create + default-first-option + :class="{'row-values-edit': isRowAuthorEdit, 'row-values-add': !isRowAuthorEdit}" + @change="setRowValues(row.items, key)" + :placeholder="$t('pleaseInput')"> + </el-select> + <span class="ky-no-br-space ksd-ml-10" v-if="!isRowAuthorEdit"> + <el-button type="primary" icon="el-ksd-icon-add_16" plain circle size="mini" @click="addRow" v-if="key==0"></el-button> + <el-button icon="el-icon-minus" plain circle size="mini" @click="removeRow(key)"></el-button> + </span> + </div> + <span slot="footer" class="dialog-footer ky-no-br-space"> + <div class="ksd-fleft"> + <el-alert + :title="$t('filterTips')" + type="info" + class="ksd-ptb-5" + :show-background="false" + :closable="false" + show-icon> + </el-alert> + </div> + <el-button plain @click="cancelRowAccess" size="medium">{{$t('kylinLang.common.cancel')}}</el-button> + <el-button type="primary" @click="submitRowAccess" size="medium">{{$t('kylinLang.common.submit')}}</el-button> + </span> + </el-dialog> + </div> + </template> + + <script> + import Vue from 'vue' + import { Component } from 'vue-property-decorator' + import { handleSuccessAsync, indexOfObjWithSomeKey, objectClone, kylinConfirm } from '../../util' + import { handleSuccess, handleError } from '../../util/business' + import { mapActions, mapGetters } from 'vuex' + import { pageSizeMapping, maxFilterAndFilterValues } from '../../config' + @Component({ + props: ['row', 'projectName'], + computed: { + ...mapGetters([ + 'isDataPermission' + ]) + }, + methods: { + ...mapActions({ + getAccessDetailsByUser: 'GET_ACCESS_DETAILS_BY_USER', + submitAccessData: 'SUBMIT_ACCESS_DATA', + getAclPermission: 'GET_ACL_PERMISSION', + changeProjectUserDataPermission: 'CHANGE_PROJECT_USER_DATA_PERMISSION' + }) + }, + locales: { + 'en': { + accessTables: 'Table Access List', + accessColumns: 'Column Access List', + accessRows: 'Row Access List', + searchKey: 'Search by table or column name', + accessTips: 'All tables in current datasource are accessible.', + accessColsTips: 'All columns in current table are accessible.', + accessRowsTips: 'All rows in current table are accessible.', + viewAllDataTips: 'After the row ACL was set, the user/user group could only access the data that match the specified filters.', + viewAllDataTips1: 'For the columns without conditions set, user/user group could access all the data.', + addRowAccess: 'Add Row ACL', + addRowAccess1: 'Add Row ACL (Table: {tableName})', + editRowAccess: 'Edit Row ACL (Table: {tableName})', + pleaseInput: 'Confirm by pressing "enter" key and separate multiple values by comma.', + loadMore: 'Load More', + tipsTitle: 'The row ACL provides IN and LIKE operators. The LIKE operator could only be used for char or varchar data type, and needs to be used with wildcards. ', + viewDetail: 'View Rules', + details: 'Details', + rules1: '_ (underscore) wildcard characters, matches any single character. ', + rules2: '% (percent) wildcard characters, matches with zero or more characters.', + rules3: '\\ (backslash) escape character. The characters following "\\" won\'t be regarded as any special characters.', + add: 'Add', + filters: 'Filter', + filterGroups: 'Filter Group', + filterTips: 'The relation between different values of the same filter is "OR"', + deleteFilterGroupTips: 'Are you sure you want to delete the filter group? All the included filters would be deleted.', + deleteFilterGroupTitle: 'Delete Filter Group', + expandAll: 'Expand All', + collapse: 'Collapse', + overFilterMaxTips: 'The number of filters or the included values of a single filter exceeds the upper limit. Please modify.', + filterTotal: 'Total number of filters: ', + filterValuesTotal: 'The filter(s) including excess values: ', + dataPermission: 'Data Permission', + dataPermissionTips: 'Allow users to access data, including viewing sample data and querying with SQL', + confirmOpen: 'Turn Open', + confirmOff: 'Turn Off', + Group: 'User Group', + User: 'User', + editACL: 'Manage Table Column, or Row Access', + openDataPermissionConfirm: 'Do you want to turn ON the data permissions of {type} "{name}"? This {type} will be able to view sample data and query.', + closeDataPermissionConfirm: 'Are you sure to turn OFF the data permissions of {type} "{name}"? This {type} will not be able to view sample data and query.' + }, + 'zh-cn': { + accessTables: '表级访问列表', + accessColumns: '列级访问列表', + accessRows: '行级访问列表', + searchKey: '搜索表名或列名', + accessTips: '当前数据源上所有表均可访问', + accessColsTips: '当前表上所有列均可访问', + accessRowsTips: '当前表上所有行均可访问', + viewAllDataTips: '设置行级权限后,用户/用户组仅能查看到表中符合筛选条件的数据。', + viewAllDataTips1: '对于没有设置权限的列,用户/用户组仍能够查看该列所有数据。', + addRowAccess: '添加行级权限', + addRowAccess1: '添加行级权限(表: {tableName})', + editRowAccess: '编辑行级权限(表: {tableName})', + pleaseInput: '请用回车进行输入确认并用逗号进行多个值分割', + loadMore: '加载更多', + tipsTitle: '行级权限支持 IN 和 LIKE 操作符。其中,LIKE 仅支持 char 和 varchar 类型的列,需配合通配符使用。', + viewDetail: '查看规则', + details: '详情', + rules1: '_(下划线)通配符,匹配任意单个字符。', + rules2: '%(百分号)通配符,匹配空白字符或任意多个字符。', + rules3: '\\(反斜杠)转义符,转义符后的通配符或转义符将不被识别为特殊字符。', + add: '添加', + filters: '过滤器', + filterGroups: '过滤组', + filterTips: '同一过滤器的不同值的关系为 “或”(OR)', + deleteFilterGroupTips: '确定要删除过滤组吗?过滤组中的过滤器会被一并删除。', + deleteFilterGroupTitle: '删除过滤组', + expandAll: '展开全部', + collapse: '收起', + overFilterMaxTips: '过滤器包含的值或过滤器总数超过上限,请修改。', + filterTotal: '过滤器总数:', + filterValuesTotal: '包含值超额的过滤器:', + dataPermission: '数据权限', + dataPermissionTips: '允许用户进行数据访问,包括查看样例数据和 SQL 查询', + confirmOpen: '开启', + confirmOff: '关闭', + Group: '用户组', + User: '用户', + editACL: '编辑表列行级访问权限', + openDataPermissionConfirm: '确定要打开{type} “{name}” 的数据权限吗?这个{type}将能查看样例数据和查询。', + closeDataPermissionConfirm: '确定要关闭{type} “{name}” 的数据权限吗?这个{type}将不能查看样例数据和查询。' + } + } + }) + export default class UserAccess extends Vue { + maxFilterAndFilterValues = maxFilterAndFilterValues + defaultProps = { + children: 'children', + label: 'label' + } + tables = [] + filterOriginDatas = [] + isEdit = false + isRerender = true + isRowAuthorEdit = false + rowAccessVisible = false + tableFilter = '' + columnFilter = '' + rowSearch = '' + columns = [] + filterCols = [] + rows = [] + rowLists = [{column_name: '', joinType: 'IN', items: []}] + row_filter = { + type: 'AND', + filter_groups: [] + } + linkKind = ['IN', 'LIKE'] + isSelectTable = false + tableAuthorizedNum = 0 + totalNum = 0 + defaultCheckedKeys = [] + defaultExpandedKeys = ['0'] + catchDefaultExpandedKeys = ['0'] + allTables = [] + copyOriginTables = [] + databaseIndex = -1 + tableIndex = -1 + editFilterGroupIndex = -1 + editRowIndex = -1 + currentTable = '' + isAllTablesAccess = false + isAllColAccess = false + colAuthorizedNum = 0 + submitLoading = false + isCurrentTableChecked = false + selectAllColumns = false + currentTableId = '' + loading = false + columnPageSize = 100 + columnCurrentPage = 1 + isAuthority = false + showDetails = false + showErrorDetails = false + isAddFilterForGroup = false + filterTotalLength = 0 + newFiltersLenth = 0 + overedRowValueFilters = [] + ext_permissions = !!this.row.ext_permissions && this.row.ext_permissions.length > 0 + get emptyText () { + return this.tableFilter ? this.$t('kylinLang.common.noResults') : this.$t('kylinLang.common.noData') + } + get emptyText2 () { + return this.columnFilter ? this.$t('kylinLang.common.noResults') : this.$t('kylinLang.common.noData') + } + get emptyText3 () { + return this.rowSearch ? this.$t('kylinLang.common.noResults') : this.$t('kylinLang.common.noData') + } + get disabledSubmitBtn () { + return JSON.stringify(this.copyOriginTables) === JSON.stringify(this.allTables) + } + get currentProjectId () { + return this.$route.query.projectId + } + showLoading () { + this.loading = true + } + hideLoading () { + this.loading = false + } + async handleChangePermission (val) { + try { + const option = { type: this.$t(this.row.type), name: this.row.role_or_name } + const msg = val ? this.$t('openDataPermissionConfirm', option) : this.$t('closeDataPermissionConfirm', option) + const confirmBtnText = val ? this.$t('confirmOpen') : this.$t('confirmOff') + await kylinConfirm(msg, {confirmButtonText: confirmBtnText, centerButton: true}, this.$t('dataPermission')) + const reqsdata = { access_entry_id: this.row.id, permissions: val ? ['DATA_QUERY'] : [], principal: this.row.type === 'User', sid: this.row.role_or_name } + const res = await this.changeProjectUserDataPermission({data: reqsdata, projectId: this.currentProjectId}) + const { data } = await handleSuccessAsync(res) + this.ext_permissions = data + this.$emit('reload') + } catch (e) { + handleError(e) + } + } + pushTableId (data) { + const index = this.catchDefaultExpandedKeys.indexOf(data.id) + if (index === -1) { + this.catchDefaultExpandedKeys.push(data.id) + } + } + removeTableId (data) { + const index = this.catchDefaultExpandedKeys.indexOf(data.id) + if (index !== -1) { + this.catchDefaultExpandedKeys.splice(index, 1) + } + } + handleNodeClick (data, node) { + if (!data.children && !data.isMore) { // tables data 中‘非加载更多’的node + this.isSelectTable = true + this.currentTable = data.database + '.' + data.label + this.isCurrentTableChecked = data.authorized + this.currentTableId = data.id + this.$refs.tableTree && this.$refs.tableTree.setCurrentKey(this.currentTableId) + const indexs = data.id.split('_') + this.databaseIndex = indexs[0] + this.tableIndex = indexs[1] + this.initColsAndRows(data.columns, data.row_filter, data.totalColNum) + } else if (!data.children && data.isMore) { + node.parent.data.currentIndex++ + const renderNums = node.parent.data.currentIndex * pageSizeMapping.TABLE_TREE + const renderMoreTables = node.parent.data.originTables.slice(0, renderNums) + node.parent.data.children = [] + node.parent.data.children = renderMoreTables + if (renderNums < node.parent.data.originTables.length) { + renderMoreTables.push(data) + } + this.reRenderTree(true) + } + } + initColsAndRows (columns, row_filter, totalColNum) { + this.columnCurrentPage = 1 + this.colAuthorizedNum = 0 + this.columns = columns.map((col) => { + if (col.authorized) { + this.colAuthorizedNum++ + } + return {name: col.column_name, authorized: col.authorized, datatype: col.datatype} + }) + this.isAllColAccess = this.colAuthorizedNum === totalColNum + this.selectAllColumns = this.isAllColAccess + this.row_filter = objectClone(row_filter) + } + getColumns (type) { + let columns = this.columns + if (type === 'LIKE') { + columns = columns.filter((c) => { + return c.datatype.indexOf('char') !== -1 || c.datatype.indexOf('varchar') !== -1 + }) + } + return columns + } + isUnCharColumn (columnName, key) { + if (columnName) { + const index = indexOfObjWithSomeKey(this.columns, 'name', columnName) + let datatype = '' + if (index !== -1) { + datatype = this.columns[index].datatype + } + const isNeedDisableLike = datatype.indexOf('char') === -1 && datatype.indexOf('varchar') === -1 + this.$set(this.rowLists[key], 'isNeedDisableLike', isNeedDisableLike) + } else { + this.$set(this.rowLists[key], 'isNeedDisableLike', false) + } + } + get checkedColumns () { + return this.columns.filter(c => c.authorized) + } + get pagedFilterColumns () { + const filterCols = objectClone(this.columns) + this.filterCols = filterCols.filter((col) => { + return col.name.toLowerCase().indexOf(this.columnFilter.trim().toLowerCase()) !== -1 + }) + return this.filterCols.slice(0, this.columnCurrentPage * this.columnPageSize) + } + loadMoreCols () { + this.columnCurrentPage++ + } + checkAllTables (val) { + this.showLoading() + setTimeout(() => { + for (let i = this.tables.length - 1; i >= 0; i--) { + this.tables[i].originTables.forEach((d) => { + if (!d.isMore) { + this.handleTableData(d, val) + this.setCurrentTable(d, val) + } + }) + } + this.setCurrentTable(this.tables[0].children[0], val) + this.reRenderTree() + this.hideLoading() + }, 500) + } + checkAllColumns (val) { + this.colAuthorizedNum = 0 + this.columns.forEach((col) => { + col.authorized = val + if (col.authorized) { + this.colAuthorizedNum++ + } + }) + const columns = this.allTables[this.databaseIndex].tables[this.tableIndex].columns + columns.forEach((col) => { + col.authorized = val + }) + this.tables[this.databaseIndex].originTables[this.tableIndex].columns = columns + } + handleTableData (data, isChecked) { + const indexs = data.id.split('_') + this.allTables[indexs[0]].tables[indexs[1]].authorized = isChecked + let database = objectClone(this.tables[indexs[0]]) + let defaultCheckedKeys = objectClone(this.defaultCheckedKeys) + if (isChecked && !data.authorized) { + this.tableAuthorizedNum++ + database.authorizedNum++ + database.label = database.databaseName + ` (${database.authorizedNum}/${database.totalNum})` + defaultCheckedKeys.push(data.id) + if (database.authorizedNum && database.authorizedNum === database.totalNum) { + defaultCheckedKeys.push(database.id) + } + } else if (!isChecked && data.authorized) { + this.tableAuthorizedNum-- + database.authorizedNum-- + database.label = database.databaseName + ` (${database.authorizedNum}/${database.totalNum})` + const removeKeyIndex = defaultCheckedKeys.indexOf(data.id) + defaultCheckedKeys.splice(removeKeyIndex, 1) + const removeDatabaseKeyIndex = defaultCheckedKeys.indexOf(database.id) + if (removeDatabaseKeyIndex !== -1) { + defaultCheckedKeys.splice(removeDatabaseKeyIndex, 1) + } + } + data.authorized = isChecked + data.columns.forEach((col) => { + col.authorized = isChecked + }) + database.originTables[indexs[1]] = data + this.tables[indexs[0]] = database + this.defaultCheckedKeys = defaultCheckedKeys + this.isAllTablesAccess = this.tableAuthorizedNum === this.totalNum + } + setCurrentTable (data, isChecked) { + const indexs = data.id.split('_') + this.databaseIndex = indexs[0] + this.tableIndex = indexs[1] + this.currentTable = data.database + '.' + data.label + this.currentTableId = data.id + this.isCurrentTableChecked = isChecked + this.selectAllColumns = isChecked + this.allTables[indexs[0]].tables[indexs[1]].columns.forEach((col) => { + col.authorized = isChecked + }) + this.allTables[indexs[0]].tables[indexs[1]].row_filter = { type: 'AND', filter_groups: [] } + this.initColsAndRows(this.allTables[indexs[0]].tables[indexs[1]].columns, this.allTables[indexs[0]].tables[indexs[1]].row_filter, data.totalColNum) + } + checkChange (data, checkNode, node) { + this.showLoading() + setTimeout(() => { + const isChecked = node.checked + if (!data.children && !data.isMore) { // tables data 中‘非加载更多’的node + this.handleTableData(data, isChecked) + this.setCurrentTable(data, isChecked) + this.reRenderTree() + } else if (data.children && data.children.length) { + data.originTables.forEach((d) => { + if (!d.isMore) { + this.handleTableData(d, isChecked) + this.setCurrentTable(d, isChecked) + } + }) + this.setCurrentTable(data.children[0], isChecked) + this.reRenderTree() + } + this.hideLoading() + }, 100) + } + reRenderTree (isLoadMoreRender) { // isLoadMoreRender 为 true 时,不重置数据 + this.isRerender = false + if (!isLoadMoreRender) { + this.tables = [...this.tables] + } + this.defaultExpandedKeys = objectClone(this.catchDefaultExpandedKeys) + this.$nextTick(() => { + this.isRerender = true + this.handleLoadMoreStyle() + setTimeout(() => { + this.$refs.tableTree.setCurrentKey(this.currentTableId) + }) + }) + } + get rowAuthorTitle () { + return !this.isRowAuthorEdit ? this.$t('addRowAccess1', {tableName: this.currentTable}) : this.$t('editRowAccess', {tableName: this.currentTable}) + } + handleLoadMoreStyle () { + this.$nextTick(() => { + const loadMore = this.$el.querySelectorAll('.acl-tree .load-more') || this.$el.getElementsByClassName('.acl-tree .load-more') + const indeterminateNodes = this.$el.querySelectorAll('.acl-tree .indeterminate-node') + if (loadMore.length) { + Array.prototype.forEach.call(loadMore, (m) => { + const targetCheckbox = m.parentNode.parentNode.querySelector('.el-checkbox') + if (targetCheckbox) { + targetCheckbox.style.display = 'none' + } + }) + } + if (indeterminateNodes.length) { + Array.prototype.forEach.call(indeterminateNodes, (n) => { + const indeterminateCheckbox = n.parentNode.parentNode.querySelector('.el-checkbox .el-checkbox__input') + if (indeterminateCheckbox) { + indeterminateCheckbox.className = 'el-checkbox__input is-indeterminate' + } + }) + } + }) + } + get filterTableData () { + let filterOriginDatas = objectClone(this.tables) + filterOriginDatas = filterOriginDatas.filter((data) => { + const originFilterTables = data.originTables.filter((t) => { + return t.label.toLowerCase().indexOf(this.tableFilter.trim().toLowerCase()) !== -1 + }) + const pagedFilterTables = originFilterTables.slice(0, pageSizeMapping.TABLE_TREE * data.currentIndex) + if (pageSizeMapping.TABLE_TREE < originFilterTables.length) { + pagedFilterTables.push({ + id: data.id + '_more', + label: this.$t('loadMore'), + class: 'load-more ksd-fs-12', + isMore: true + }) + } + data.children = pagedFilterTables + return pagedFilterTables.length > 0 + }) + this.defaultExpandedKeys = objectClone(this.catchDefaultExpandedKeys) + this.$nextTick(() => { + this.handleLoadMoreStyle() + if (this.$refs.tableTree) { + this.$refs.tableTree.setCurrentKey(this.currentTableId) + } + }) + return filterOriginDatas + } + isShowRow (row) { + let isShow = false + if (row.column_name.toLowerCase().indexOf(this.rowSearch.trim().toLowerCase()) !== -1) { + isShow = true + } + for (let i = 0; i < row.in_items.length; i++) { + if (row.in_items[i].toLowerCase().indexOf(this.rowSearch.trim().toLowerCase()) !== -1) { + isShow = true + break + } + } + for (let k = 0; k < row.like_items.length; k++) { + if (row.like_items[k].toLowerCase().indexOf(this.rowSearch.trim().toLowerCase()) !== -1) { + isShow = true + break + } + } + return isShow + } + editAccess () { + this.isEdit = true + this.loadAccessDetails(false) + } + cancelAccess () { + this.isEdit = false + this.loadAccessDetails(true) + } + submitAccess () { + this.submitLoading = true + this.submitAccessData({projectName: this.projectName, userType: this.row.type, roleOrName: this.row.role_or_name, accessData: this.allTables}).then((res) => { + handleSuccess(res, () => { + this.$message({ + type: 'success', + message: this.$t('kylinLang.common.submitSuccess') + }) + this.submitLoading = false + this.isEdit = false + this.loadAccessDetails(true) + }) + }, (res) => { + handleError(res) + this.submitLoading = false + }) + } + selectColumn (val, col) { + let columns = this.allTables[this.databaseIndex].tables[this.tableIndex].columns + const index = indexOfObjWithSomeKey(columns, 'column_name', col) + columns[index].authorized = val + this.tables[this.databaseIndex].originTables[this.tableIndex].columns = columns + this.columns[index].authorized = val + if (val) { + this.colAuthorizedNum++ + } else { + this.colAuthorizedNum-- + } + this.selectAllColumns = this.colAuthorizedNum === this.columns.length + } + setRowValues (values, key) { + let formatValues = [] + values.forEach((v) => { + const val = v.trim().split(/[,,]/g) + formatValues = [...formatValues, ...val] + }) + this.rowLists[key].items = formatValues.filter(it => !!it) + } + addRowAccess (fgIndex) { + if (fgIndex !== -1) { + this.editFilterGroupIndex = fgIndex + this.isAddFilterForGroup = true // 在指定筛选组里添加筛选器 + } else { + this.editFilterGroupIndex = fgIndex + this.isAddFilterForGroup = false + } + this.filterTotalLength = 0 + this.overedRowValueFilters = [] + this.isRowAuthorEdit = false + this.rowAccessVisible = true + } + addFilterGroups () { + this.row_filter.filter_groups.push({is_group: true, type: 'AND', filters: []}) + } + getSearchFilterdGroup (filterGroups) { // 有搜索关键时,如果没有符合搜索的过滤器内容,去掉空的过滤组 + if (this.rowSearch) { + return filterGroups.filter(fg => { + return this.getFilterFilters(fg.filters).length > 0 + }) + } else { + return filterGroups + } + } + getFilterFilters (filters) { + return filters.filter((f) => { + return this.isShowRow(f) + }) + } + getlimitFilters (fg) { + const filterFilters = this.getFilterFilters(fg.filters) + if (filterFilters.length <= 3 || (fg.isExpand && filterFilters.length > 3)) { + return filterFilters + } else { + return filterFilters.slice(0, 3) + } + } + toggleExpandFG (fg) { + this.$set(fg, 'isExpand', !fg.isExpand) + } + changeRowFilterType () { + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.type = this.row_filter.type + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.type = this.row_filter.type + } + changeFilterGroupTpye (fgIndex) { + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.filter_groups[fgIndex].type = this.row_filter.filter_groups[fgIndex].type + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.filter_groups[fgIndex].type = this.row_filter.filter_groups[fgIndex].type + } + editRowAccess (fgIndex, index, row) { + this.isRowAuthorEdit = true + this.editFilterGroupIndex = fgIndex + this.editRowIndex = index + this.rowLists = [] + if (row.in_items.length > 0) { + this.rowLists.push({column_name: row.column_name, joinType: 'IN', items: row.in_items}) + } + if (row.like_items.length > 0) { + this.rowLists.push({column_name: row.column_name, joinType: 'LIKE', items: row.like_items}) + } + this.filterTotalLength = 0 + this.overedRowValueFilters = [] + this.rowAccessVisible = true + } + deleteRowAccess (fgIndex, index, row) { + let idx = index + this.row_filter.filter_groups[fgIndex].filters.splice(idx, 1) + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.filter_groups[fgIndex].filters.splice(idx, 1) + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.filter_groups[fgIndex].filters.splice(idx, 1) + if (!this.row_filter.filter_groups[fgIndex].filters.length) { + if (!this.row_filter.filter_groups[fgIndex].is_group) { + this.row_filter.filter_groups.splice(fgIndex, 1) + } + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.filter_groups.splice(fgIndex, 1) + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.filter_groups.splice(fgIndex, 1) + } + } + async deleteFG (fgIndex) { + await kylinConfirm(this.$t('deleteFilterGroupTips'), {confirmButtonText: this.$t('kylinLang.common.delete'), centerButton: true}, this.$t('deleteFilterGroupTitle')) + this.row_filter.filter_groups.splice(fgIndex, 1) + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.filter_groups.splice(fgIndex, 1) + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.filter_groups.splice(fgIndex, 1) + } + cancelRowAccess () { + this.rowAccessVisible = false + this.filterTotalLength = 0 + this.overedRowValueFilters = [] + } + fromRowArrToObj (rowsList, key) { + var len = rowsList && rowsList.length || 0 + var obj = {} + for (var k = 0; k < len; k++) { + if (rowsList[k].items.length) { + obj[rowsList[k][key]] = obj[rowsList[k][key]] || [] + obj[rowsList[k][key]] = [...obj[rowsList[k][key]], ...rowsList[k].items] + } + } + return obj + } + checkFilterValues (filters) { + const overedRowValueFilters = [] + filters.forEach(f => { + let copyRowList = objectClone(this.rowLists) + let index = indexOfObjWithSomeKey(copyRowList, 'column_name', f.column_name) + let newLenth = 0 + while (index !== -1) { + newLenth = newLenth + copyRowList[index].items.length + copyRowList.splice(index, 1) + index = indexOfObjWithSomeKey(copyRowList, 'column_name', f.column_name) + } + if (f.in_items.length + f.like_items.length + newLenth > maxFilterAndFilterValues) { // 单个 filter 中的值的数量最多支持 100 个 (包含LIKE) + overedRowValueFilters.push(f.column_name + `(${f.in_items.length + f.like_items.length + newLenth}/${maxFilterAndFilterValues})`) + } + }) + return overedRowValueFilters + } + checkMaxRowAccess () { + this.filterTotalLength = 0 // filter 总数不超过 100 个 (包含 group 中的 filter) + this.newFiltersLenth = 0 + this.overedRowValueFilters = [] + if (!this.isRowAuthorEdit) { // 新添加过滤器的入口 + if (!this.isAddFilterForGroup) { + for (let fgKey in this.row_filter.filter_groups) { + const fg = this.row_filter.filter_groups[fgKey] + if (fg.is_group) continue // 添加外层过滤器,只在外层过滤器里比对列名值是否超过max + this.overedRowValueFilters = this.checkFilterValues(fg.filters) + } + } else { + const fg = this.row_filter.filter_groups[this.editFilterGroupIndex] + this.overedRowValueFilters = this.checkFilterValues(fg.filters) + } + if (!this.overedRowValueFilters.length) { // overedRowValueFilters 为空说明已存在的filter没有超过max的值 + this.row_filter.filter_groups.forEach(fg => { + this.filterTotalLength = this.filterTotalLength + fg.filters.length + }) + const rowsObject = this.fromRowArrToObj(this.rowLists, 'column_name') + this.newFiltersLenth = Object.keys(rowsObject).length + for (let key in rowsObject) { + if (rowsObject[key].length > maxFilterAndFilterValues) { // 新增的单条过滤值超过max + this.overedRowValueFilters.push(key + `(${rowsObject[key].length}/${maxFilterAndFilterValues})`) + } + } + } + return (this.filterTotalLength + this.newFiltersLenth > maxFilterAndFilterValues) || this.overedRowValueFilters.length > 0 + } else { // 编辑是同一列的in 和 like 值已合并 + const rowsObject = this.fromRowArrToObj(this.rowLists, 'column_name') + for (let key in rowsObject) { + if (rowsObject[key].length > maxFilterAndFilterValues) { // 新增的单条过滤值超过max + this.overedRowValueFilters.push(key + `(${rowsObject[key].length}/${maxFilterAndFilterValues})`) + } + } + return this.overedRowValueFilters.length > 0 + } + } + submitRowAccess () { + if (this.checkMaxRowAccess()) return + if (!this.isRowAuthorEdit) { + this.rowLists.forEach((row) => { + if (row.column_name && row.items.length) { + if (this.isAddFilterForGroup) { // 只在指定筛选组里合并相同维度 + const filters = this.row_filter.filter_groups[this.editFilterGroupIndex].filters + const index = indexOfObjWithSomeKey(filters, 'column_name', row.column_name) + if (index !== -1) { + if (row.joinType === 'IN') { + filters[index].in_items = [...filters[index].in_items, ...row.items] + } else if (row.joinType === 'LIKE') { + filters[index].like_items = [...filters[index].like_items, ...row.items] + } + } else { + const rowObj = {column_name: row.column_name, in_items: row.joinType === 'IN' ? row.items : [], like_items: row.joinType === 'LIKE' ? row.items : []} + filters.push(rowObj) + } + } else { // 在筛选组外的所有筛选器合并相同维度 + let isExistedColumn = false + for (let fg in this.row_filter.filter_groups) { + if (this.row_filter.filter_groups[fg].is_group) continue + const index = indexOfObjWithSomeKey(this.row_filter.filter_groups[fg].filters, 'column_name', row.column_name) + if (index !== -1) { + if (row.joinType === 'IN') { + this.row_filter.filter_groups[fg].filters[index].in_items = [...this.row_filter.filter_groups[fg].filters[index].in_items, ...row.items] + } else if (row.joinType === 'LIKE') { + this.row_filter.filter_groups[fg].filters[index].like_items = [...this.row_filter.filter_groups[fg].filters[index].like_items, ...row.items] + } + isExistedColumn = true + break + } + } + if (!isExistedColumn) { // 新增的列,需要新增一个filterGroups, is_group 为false + const rowObj = {column_name: row.column_name, in_items: row.joinType === 'IN' ? row.items : [], like_items: row.joinType === 'LIKE' ? row.items : []} + this.row_filter.filter_groups.push({is_group: false, type: 'AND', filters: [rowObj]}) + } + } + } + }) + } else { + this.rowLists.forEach((r) => { + if (r.joinType === 'IN') { + this.row_filter.filter_groups[this.editFilterGroupIndex].filters[this.editRowIndex].in_items = r.items + } else if (r.joinType === 'LIKE') { + this.row_filter.filter_groups[this.editFilterGroupIndex].filters[this.editRowIndex].like_items = r.items + } + }) + if (this.row_filter.filter_groups[this.editFilterGroupIndex].filters[this.editRowIndex].in_items.length === 0 && this.row_filter.filter_groups[this.editFilterGroupIndex].filters[this.editRowIndex].like_items.length === 0) { + this.row_filter.filter_groups[this.editFilterGroupIndex].filters.splice(this.editRowIndex, 1) + if (!this.row_filter.filter_groups[this.editFilterGroupIndex].filters.length) { + if (!this.row_filter.filter_groups[this.editFilterGroupIndex].is_group) { + this.row_filter.filter_groups.splice(this.editFilterGroupIndex, 1) + } + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter.filter_groups.splice(this.editFilterGroupIndex, 1) + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter.filter_groups.splice(this.editFilterGroupIndex, 1) + } + } + } + this.allTables[this.databaseIndex].tables[this.tableIndex].row_filter = objectClone(this.row_filter) + this.tables[this.databaseIndex].originTables[this.tableIndex].row_filter = objectClone(this.row_filter) + this.rowAccessVisible = false + } + addRow () { + this.rowLists.unshift({column_name: '', joinType: 'IN', items: []}) + } + removeRow (index) { + this.rowLists.splice(index, 1) + } + resetRowAccess () { + this.showErrorDetails = false + this.showDetails = false + this.rowLists = [{column_name: '', joinType: 'IN', items: []}] + } + async loadAccessDetails (authorizedOnly) { + this.defaultCheckedKeys = [] + this.allTables = [] + this.tables = [] + this.rows = [] + this.columns = [] + const response = await this.getAccessDetailsByUser({data: {authorized_only: authorizedOnly, project: this.projectName}, roleOrName: this.row.role_or_name, type: this.row.type, projectName: this.projectName}) + const result = await handleSuccessAsync(response) + if (result.length) { + this.allTables = objectClone(result) + this.copyOriginTables = objectClone(result) + this.tableAuthorizedNum = 0 + this.totalNum = 0 + this.currentTableId = '' + this.tables = result.map((database, key) => { + this.tableAuthorizedNum = this.tableAuthorizedNum + database.authorized_table_num + this.totalNum = this.totalNum + database.total_table_num + const labelNum = this.authorizedOnly ? ` (${database.authorized_table_num})` : ` (${database.authorized_table_num}/${database.total_table_num})` + const originTableDatas = database.tables.map((t, i) => { + const id = key + '_' + i + if (t.authorized && !authorizedOnly) { + this.defaultCheckedKeys.push(id) + } + if (database.database_name + '.' + t.table_name === this.currentTable) { + this.currentTableId = id + } + return {id: id, label: t.table_name, database: database.database_name, authorized: t.authorized, columns: t.columns, row_filter: t.row_filter, totalColNum: t.total_column_num} + }) + const pegedTableDatas = originTableDatas.slice(0, pageSizeMapping.TABLE_TREE) + if (pageSizeMapping.TABLE_TREE < originTableDatas.length) { + pegedTableDatas.push({ + id: key + '_more', + label: this.$t('loadMore'), + class: 'load-more ksd-fs-12', + isMore: true + }) + } + if (database.authorized_table_num && database.authorized_table_num === database.total_table_num) { + this.defaultCheckedKeys.push(key + '') + } + return { + id: key + '', + label: database.database_name + labelNum, + class: database.authorized_table_num && database.authorized_table_num < database.total_table_num ? 'indeterminate-node' : '', + databaseName: database.database_name, + authorizedNum: database.authorized_table_num, + totalNum: database.total_table_num, + originTables: originTableDatas, + currentIndex: 1, + children: pegedTableDatas + } + }) + this.isAllTablesAccess = this.tableAuthorizedNum === this.totalNum + this.$nextTick(() => { + if (this.currentTableId) { + const indexs = this.currentTableId.split('_') + if (indexs[1] <= pageSizeMapping.TABLE_TREE) { + this.handleNodeClick(this.tables[indexs[0]].children[indexs[1]]) + } else { + this.handleNodeClick(this.tables[0].children[0]) + } + } else { + this.handleNodeClick(this.tables[0].children[0]) + } + this.handleLoadMoreStyle() + }) + } + } + created () { + this.loadAccessDetails(true) + this.getAclPermission({project: this.projectName}).then((res) => { + const { data } = res.data + handleSuccess(res, () => { + this.isAuthority = data + }) + }, (err) => { + handleError(err) + }) + } + } + </script> + + <style lang="less"> + @import '../../assets/styles/variables.less'; + .data-permission { + .flex { + display: flex; + align-items: center; + } + .el-icon-ksd-info { + color: @text-placeholder-color; + } + } + .access-content { + .join-type { + width: 70px; + } + .row-list { + .el-col { + display: block !important; + word-break: break-all; + line-height: 1.4; + padding: 5px; + } + } + } + .row-column { + width: 210px; + } + .row-values-edit { + width: calc(~'100% - 295px'); + } + .row-values-add { + width: calc(~'100% - 365px'); + } + .row-values-edit, + .row-values-add { + .el-select__input { + margin-left: 12px; + } + .el-select__tags > span { + max-width: 100%; + .el-tag { + max-width: 100%; + position: relative; + padding-right: 16px; + white-space: normal; + word-break: break-word; + .el-select__tags-text { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + } + .el-tag__close { + position: absolute; + right: 1px; + // top: 5px; + } + } + } + } + .author_dialog { + .el-dialog { + position: absolute; + left: 0; + right: 0; + margin: auto; + max-height: 70%; + overflow: hidden; + display: flex; + flex-direction: column; + .el-dialog__header { + min-height: 47px; + } + .el-dialog__body { + overflow: auto; + } + } + } + .a-like { + color: @base-color; + position: relative; + .arrow { + transform: rotate(90deg) scale(0.8); + margin-left: 3px; + font-size: 7px; + color: @base-color; + position: absolute; + top: 2px; + } + } + .error-msg { + color: @text-normal-color; + word-break: break-all; + } + .like-tips-block { + color: @text-normal-color; + .review-details { + color: @base-color; + cursor: pointer; + position: relative; + } + .el-icon-ksd-more_01-copy { + transform: scale(0.8); + } + .arrow { + transform: rotate(90deg) scale(0.8); + margin-left: 3px; + font-size: 7px; + color: @base-color; + position: absolute; + top: 4px; + } + } + .author_dialog { + .detail-content { + background-color: @base-background-color-1; + padding: 10px 15px; + box-sizing: border-box; + font-size: 12px; + color: @text-normal-color; + } + } + </style> diff --git a/kystudio/src/router/routerGuard.js b/kystudio/src/router/routerGuard.js index c256905682..21761a4767 100644 --- a/kystudio/src/router/routerGuard.js +++ b/kystudio/src/router/routerGuard.js @@ -55,8 +55,9 @@ export function bindRouterGuard (router) { // 判断用户是否有当前所要进入的页面权限 let getRouteAuthority = (source) => { // 获取该用户所有有权限的菜单 - const defaultMenus = getAvailableOptions('menu', { groupRole: store.getters.userAuthorities, projectRole: store.state.user.currentUserAccess, menu: 'dashboard' }) - const adminMenus = getAvailableOptions('menu', { groupRole: store.getters.userAuthorities, projectRole: store.state.user.currentUserAccess, menu: 'project' }) + const dataPermission = store.state.user.ext_permissions ? 'dataPermission' : 'noDataPermission' + const defaultMenus = getAvailableOptions('menu', { groupRole: store.getters.userAuthorities, projectRole: store.state.user.currentUserAccess, dataPermission, menu: 'dashboard' }) + const adminMenus = getAvailableOptions('menu', { groupRole: store.getters.userAuthorities, projectRole: store.state.user.currentUserAccess, dataPermission, menu: 'project' }) let availableMenus = [...defaultMenus, ...adminMenus] let auth = to.name && availableMenus.includes(to.name.toLowerCase()) diff --git a/kystudio/src/service/project.js b/kystudio/src/service/project.js index 9ba40a48bc..7c71495818 100644 --- a/kystudio/src/service/project.js +++ b/kystudio/src/service/project.js @@ -149,6 +149,9 @@ export default { getDefaultConfig () { return Vue.resource(apiUrl + 'projects/default_configs').get() }, + changeProjectUserDataPermission (params) { + return Vue.resource(apiUrl + `access/extension/ProjectInstance/${params.projectId}`).update(params.data) + }, loadExcludeTables (params) { return Vue.resource(apiUrl + `tables/excluded_tables`).get(params) }, diff --git a/kystudio/src/service/system.js b/kystudio/src/service/system.js index ffb1ac73f3..98a5adbb61 100644 --- a/kystudio/src/service/system.js +++ b/kystudio/src/service/system.js @@ -38,8 +38,8 @@ export default { }, // 生成诊断包相关api接口 getDumpRemote: (para) => { - const { host, start, end, job_id } = para - return Vue.http.post(apiUrl + `system/diag?host=${host}`, { start, end, job_id }) + const { host, start, end, job_id, project } = para + return Vue.http.post(apiUrl + `system/diag?host=${host}`, { start, end, job_id, project }) }, // 获取诊断包生成进度 getStatusRemote: (para) => { diff --git a/kystudio/src/service/user.js b/kystudio/src/service/user.js index 6d59ae2bd1..8d8cd15461 100644 --- a/kystudio/src/service/user.js +++ b/kystudio/src/service/user.js @@ -34,7 +34,7 @@ export default { return Vue.resource(apiUrl + 'user/authentication').get() }, userAccess: (para) => { - return Vue.resource(apiUrl + 'access/permission/project_permission').get(para) + return Vue.resource(apiUrl + 'access/permission/project_ext_permission').get(para) }, // user goup addGroupsToUser: (para) => { @@ -60,5 +60,11 @@ export default { }, getAccessDetailsByUser: (projectName, roleOrName, data, type) => { return Vue.resource(apiUrl + `acl/${type}/${roleOrName}`).get(data) + }, + getCurrentUserDataPermission: (para) => { + return Vue.resource(apiUrl + `access/global/permission/data_query/${para.username}`).get() + }, + updataUserDataPermission: (para) => { + return Vue.resource(apiUrl + 'access/global/permission/data_query').update(para) } } diff --git a/kystudio/src/store/config.js b/kystudio/src/store/config.js index 019f3371e4..3f3c378bfd 100644 --- a/kystudio/src/store/config.js +++ b/kystudio/src/store/config.js @@ -85,8 +85,9 @@ export default { const groupRole = rootGetters.userAuthorities const projectRole = rootState.user.currentUserAccess const menu = rootState.route.name.toLowerCase() + const dataPermission = rootState.user.ext_permissions ? 'dataPermission' : 'noDataPermission' - return getAvailableOptions('menu', { groupRole, projectRole, menu }) + return getAvailableOptions('menu', { groupRole, projectRole, dataPermission, menu }) } else { return [] } diff --git a/kystudio/src/store/project.js b/kystudio/src/store/project.js index a5b9a8e7d8..8241593a2b 100644 --- a/kystudio/src/store/project.js +++ b/kystudio/src/store/project.js @@ -322,6 +322,9 @@ export default { }, [types.UPDATE_EXCLUDE_COLUMN_CONFIG]: function ({ commit }, para) { return api.project.updateExcludeColumnConfig(para) + }, + [types.CHANGE_PROJECT_USER_DATA_PERMISSION]: function ({ commit }, para) { + return api.project.changeProjectUserDataPermission(para) } }, getters: { diff --git a/kystudio/src/store/types.js b/kystudio/src/store/types.js index d852acf269..7ec5311e22 100644 --- a/kystudio/src/store/types.js +++ b/kystudio/src/store/types.js @@ -71,6 +71,7 @@ export const UPDATE_PROJECT_CONFIG = 'UPDATE_PROJECT_CONFIG' export const DELETE_PROJECT_CONFIG = 'DELETE_PROJECT_CONFIG' export const GET_DEFAULT_CONFIG = 'GET_DEFAULT_CONFIG' export const SAVE_DEFAULT_CONFIG_LIST = 'SAVE_DEFAULT_CONFIG_LIST' +export const CHANGE_PROJECT_USER_DATA_PERMISSION = 'CHANGE_PROJECT_USER_DATA_PERMISSION' // datasource actions mutations // csv 数据源 export const VERIFY_CSV_CONN = 'VERIFY_CSV_CONN' @@ -364,6 +365,8 @@ export const GET_CANARY_REPORT = 'GET_CANARY_REPORT' export const SAVE_CANARY_REPORT = 'SAVE_CANARY_REPORT' export const INIT_SPEC = 'INIT_SPEC' export const COLLECT_MESSAGE_DIRECTIVES = 'COLLECT_MESSAGE_DIRECTIVES' +export const GET_CURRENT_USER_DATA_PERMISSION = 'GET_CURRENT_USER_DATA_PERMISSION' +export const UPDATE_USER_DATA_PERMISSION = 'UPDATE_USER_DATA_PERMISSION' // monitor actions mutations export const SUGGEST_MODEL = 'SUGGEST_MODEL' export const SAVE_SUGGEST_MODELS = 'SAVE_SUGGEST_MODELS' diff --git a/kystudio/src/store/user.js b/kystudio/src/store/user.js index 0f5b00bef0..fbba4e3fd4 100644 --- a/kystudio/src/store/user.js +++ b/kystudio/src/store/user.js @@ -11,6 +11,7 @@ export default { usersGroupSize: 0, currentUser: null, currentUserAccess: 'DEFAULT', + ext_permissions: false, userDetail: null, isShowAdminTips: !cacheLocalStorage('isHideAdminTips') }, @@ -27,7 +28,8 @@ export default { state.currentUser = result.user }, [types.SAVE_CURRENT_USER_ACCESS]: function (state, result) { - state.currentUserAccess = result.access + state.currentUserAccess = result.data.permission + state.ext_permissions = result.data.ext_permissions[0] === 'DATA_QUERY' }, [types.RESET_CURRENT_USER]: function (state) { state.currentUser = null @@ -110,7 +112,7 @@ export default { return new Promise((resolve, reject) => { api.user.userAccess({project: para.project}).then((res) => { if (!para.not_cache) { - commit(types.SAVE_CURRENT_USER_ACCESS, {access: res.data.data}) + commit(types.SAVE_CURRENT_USER_ACCESS, {data: res.data.data}) } resolve(res) }, () => { @@ -120,6 +122,12 @@ export default { }, [types.GET_ACCESS_DETAILS_BY_USER]: function ({ commit }, para) { return api.user.getAccessDetailsByUser(para.projectName, para.roleOrName, para.data, para.type) + }, + [types.GET_CURRENT_USER_DATA_PERMISSION]: function ({ commit }, para) { + return api.user.getCurrentUserDataPermission(para) + }, + [types.UPDATE_USER_DATA_PERMISSION]: function ({ commit }, para) { + return api.user.updataUserDataPermission(para) } }, getters: { @@ -159,6 +167,9 @@ export default { permissions.ADMINISTRATION.value, permissions.MANAGEMENT.value ].includes(state.currentUserAccess) + }, + isDataPermission (state) { + return state.ext_permissions } } }