You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by xx...@apache.org on 2023/07/06 06:18:35 UTC
[kylin] 03/11: KYLIN-5594 data permission
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 <la...@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
}
}
}