You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/07/04 05:59:20 UTC
[cloudstack-primate] branch master updated: iam: UI changes for
Dynamic roles improvements (#353)
This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new 6bb8173 iam: UI changes for Dynamic roles improvements (#353)
6bb8173 is described below
commit 6bb8173e8fef491fc0bf04096046f37754de7d6b
Author: sureshanaparti <12...@users.noreply.github.com>
AuthorDate: Sat Jul 4 11:29:11 2020 +0530
iam: UI changes for Dynamic roles improvements (#353)
This PR addresses the below UI improvements for current Dynamic roles functionality.
Export rules of a role to a CSV file, with name: .csv_
Import a role with its rules using a CSV file.
Create a role from any of the existing role.
Co-authored-by: davidjumani <dj...@gmail.com>
---
src/config/section/role.js | 16 +-
src/locales/en.json | 7 +
src/views/iam/CreateRole.vue | 205 ++++++++++++++++++++++++
src/views/iam/ImportRole.vue | 299 ++++++++++++++++++++++++++++++++++++
src/views/iam/RolePermissionTab.vue | 48 +++++-
5 files changed, 564 insertions(+), 11 deletions(-)
diff --git a/src/config/section/role.js b/src/config/section/role.js
index 3dccfa0..cefe1e1 100644
--- a/src/config/section/role.js
+++ b/src/config/section/role.js
@@ -36,12 +36,16 @@ export default {
icon: 'plus',
label: 'label.add.role',
listView: true,
- args: ['name', 'description', 'type'],
- mapping: {
- type: {
- options: ['Admin', 'DomainAdmin', 'User']
- }
- }
+ popup: true,
+ component: () => import('@/views/iam/CreateRole.vue')
+ },
+ {
+ api: 'importRole',
+ icon: 'cloud-upload',
+ label: 'label.import.role',
+ listView: true,
+ popup: true,
+ component: () => import('@/views/iam/ImportRole.vue')
},
{
api: 'updateRole',
diff --git a/src/locales/en.json b/src/locales/en.json
index 4b64d55..78ad666 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -449,6 +449,7 @@
"label.baremetalcpucores": "# of CPU Cores",
"label.baremetalmac": "Host MAC",
"label.baremetalmemory": "Memory (in MB)",
+"label.based.on": "Based on",
"label.basic": "Basic",
"label.basic.mode": "Basic Mode",
"label.basicsetup": "Basic setup",
@@ -777,6 +778,9 @@
"label.enter.token": "Enter token",
"label.error": "Error",
"label.error.code": "Error Code",
+"label.error.file.upload": "File upload failed",
+"label.error.file.read": "Cannot read file",
+"label.error.rules.file.import": "Please choose a valid CSV rules file",
"label.error.something.went.wrong.please.correct.the.following": "Something went wrong; please correct the following",
"label.error.upper": "ERROR",
"label.error.volume.upload": "Please choose a file",
@@ -937,6 +941,7 @@
"label.ikepolicy": "IKE policy",
"label.images": "Images",
"label.import.backup.offering": "Import Backup Offering",
+"label.import.role": "Import Role",
"label.info": "Info",
"label.info.upper": "INFO",
"label.infrastructure": "Infrastructure",
@@ -1665,6 +1670,8 @@
"label.rule": "Rule",
"label.rule.number": "Rule Number",
"label.rules": "Rules",
+"label.rules.file": "Rules File",
+"label.rules.file.import.description": "Click or drag rule defintions CVS file to import",
"label.running": "Running VMs",
"label.saml.disable": "SAML Disable",
"label.saml.enable": "SAML Enable",
diff --git a/src/views/iam/CreateRole.vue b/src/views/iam/CreateRole.vue
new file mode 100644
index 0000000..23730b5
--- /dev/null
+++ b/src/views/iam/CreateRole.vue
@@ -0,0 +1,205 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+ <div class="form-layout">
+ <a-spin :spinning="loading">
+ <a-form
+ :form="form"
+ @submit="handleSubmit"
+ layout="vertical">
+ <a-form-item :label="$t('label.name')">
+ <a-input
+ v-decorator="['name', {
+ rules: [{ required: true, message: $t('message.error.required.input') }]
+ }]"
+ :placeholder="createRoleApiParams.name.description" />
+ </a-form-item>
+
+ <a-form-item :label="$t('label.description')">
+ <a-input
+ v-decorator="['description']"
+ :placeholder="createRoleApiParams.description.description" />
+ </a-form-item>
+
+ <a-form-item :label="$t('label.based.on')" v-if="'roleid' in createRoleApiParams">
+ <a-radio-group
+ v-decorator="['using', {
+ initialValue: this.createRoleUsing
+ }]"
+ buttonStyle="solid"
+ @change="selected => { this.handleChangeCreateRole(selected.target.value) }">
+ <a-radio-button value="type">
+ {{ $t('label.type') }}
+ </a-radio-button>
+ <a-radio-button value="role">
+ {{ $t('label.role') }}
+ </a-radio-button>
+ </a-radio-group>
+ </a-form-item>
+
+ <a-form-item :label="$t('label.type')" v-if="this.createRoleUsing === 'type'">
+ <a-select
+ v-decorator="['type', {
+ rules: [{ required: true, message: $t('message.error.select') }]
+ }]"
+ :placeholder="createRoleApiParams.type.description">
+ <a-select-option v-for="role in defaultRoles" :key="role">
+ {{ role }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item :label="$t('label.role')" v-if="this.createRoleUsing === 'role'">
+ <a-select
+ v-decorator="['roleid', {
+ rules: [{ required: true, message: $t('message.error.select') }]
+ }]"
+ :placeholder="createRoleApiParams.roleid.description">
+ <a-select-option
+ v-for="role in roles"
+ :value="role.id"
+ :key="role.id">
+ {{ role.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
+ <a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
+ </div>
+ </a-form>
+ </a-spin>
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'CreateRole',
+ data () {
+ return {
+ roles: [],
+ defaultRoles: ['Admin', 'DomainAdmin', 'ResourceAdmin', 'User'],
+ createRoleUsing: 'type',
+ loading: false
+ }
+ },
+ mounted () {
+ this.fetchRoles()
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ this.apiConfig = this.$store.getters.apis.createRole || {}
+ this.createRoleApiParams = {}
+ this.apiConfig.params.forEach(param => {
+ this.createRoleApiParams[param.name] = param
+ })
+ },
+ watch: {
+ '$route' (to, from) {
+ if (to.fullPath !== from.fullPath && !to.fullPath.includes('action/')) {
+ this.fetchRoles()
+ }
+ },
+ '$i18n.locale' (to, from) {
+ if (to !== from) {
+ this.fetchRoles()
+ }
+ }
+ },
+ methods: {
+ handleSubmit (e) {
+ e.preventDefault()
+ this.form.validateFields((err, values) => {
+ if (err) {
+ return
+ }
+ const params = {}
+ for (const key in values) {
+ if (key === 'using') {
+ continue
+ }
+
+ const input = values[key]
+ if (input === undefined) {
+ continue
+ }
+
+ params[key] = input
+ }
+
+ this.createRole(params)
+ })
+ },
+ closeAction () {
+ this.$emit('close-action')
+ },
+ createRole (params) {
+ this.loading = true
+ api('createRole', params).then(json => {
+ const role = json.createroleresponse.role
+ if (role) {
+ this.$emit('refresh-data')
+ this.$notification.success({
+ message: 'Create Role',
+ description: 'Sucessfully created role ' + params.name
+ })
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.loading = false
+ this.closeAction()
+ })
+ },
+ fetchRoles () {
+ const params = {}
+ api('listRoles', params).then(json => {
+ if (json && json.listrolesresponse && json.listrolesresponse.role) {
+ this.roles = json.listrolesresponse.role
+ }
+ }).catch(error => {
+ console.error(error)
+ })
+ },
+ handleChangeCreateRole (value) {
+ this.createRoleUsing = value
+ }
+ }
+}
+</script>
+
+<style scoped lang="less">
+ .form-layout {
+ width: 80vw;
+
+ @media (min-width: 700px) {
+ width: 550px;
+ }
+ }
+
+ .action-button {
+ text-align: right;
+
+ button {
+ margin-right: 5px;
+ }
+ }
+</style>
diff --git a/src/views/iam/ImportRole.vue b/src/views/iam/ImportRole.vue
new file mode 100644
index 0000000..64c6ee5
--- /dev/null
+++ b/src/views/iam/ImportRole.vue
@@ -0,0 +1,299 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+ <div class="form-layout">
+ <a-spin :spinning="loading">
+ <a-form
+ :form="form"
+ @submit="handleSubmit"
+ layout="vertical">
+ <a-form-item :label="$t('label.rules.file')">
+ <a-upload-dragger
+ :multiple="false"
+ :fileList="fileList"
+ :remove="handleRemove"
+ :beforeUpload="beforeUpload"
+ @change="handleChange"
+ v-decorator="['file', {
+ rules: [{ required: true, message: $t('message.error.required.input') },
+ {
+ validator: checkCsvRulesFile,
+ message: $t('label.error.rules.file.import')
+ }
+ ]
+ }]">
+ <p class="ant-upload-drag-icon">
+ <a-icon type="cloud-upload" />
+ </p>
+ <p class="ant-upload-text" v-if="fileList.length === 0">
+ {{ $t('label.rules.file.import.description') }}
+ </p>
+ </a-upload-dragger>
+ </a-form-item>
+ <a-form-item :label="$t('label.name')">
+ <a-input
+ v-decorator="['name', {
+ rules: [{ required: true, message: $t('message.error.required.input') }]
+ }]"
+ :placeholder="importRoleApiParams.name.description" />
+ </a-form-item>
+
+ <a-form-item :label="$t('label.description')">
+ <a-input
+ v-decorator="['description']"
+ :placeholder="importRoleApiParams.description.description" />
+ </a-form-item>
+
+ <a-form-item :label="$t('label.type')">
+ <a-select
+ v-decorator="['type', {
+ rules: [{ required: true, message: $t('message.error.select') }]
+ }]"
+ :placeholder="importRoleApiParams.type.description">
+ <a-select-option v-for="role in defaultRoles" :key="role">
+ {{ role }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item :label="$t('label.forced')">
+ <a-switch
+ v-decorator="['forced', {
+ initialValue: false
+ }]" />
+ </a-form-item>
+
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
+ <a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
+ </div>
+ </a-form>
+ </a-spin>
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'ImportRole',
+ data () {
+ return {
+ fileList: [],
+ defaultRoles: ['Admin', 'DomainAdmin', 'ResourceAdmin', 'User'],
+ rulesCsv: '',
+ loading: false
+ }
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ this.apiConfig = this.$store.getters.apis.importRole || {}
+ this.importRoleApiParams = {}
+ this.apiConfig.params.forEach(param => {
+ this.importRoleApiParams[param.name] = param
+ })
+ },
+ methods: {
+ handleRemove (file) {
+ const index = this.fileList.indexOf(file)
+ const newFileList = this.fileList.slice()
+ newFileList.splice(index, 1)
+ this.fileList = newFileList
+ },
+ handleChange (info) {
+ if (info.file.status === 'error') {
+ this.$notification.error({
+ message: this.$t('label.error.file.upload'),
+ description: this.$t('label.error.file.upload')
+ })
+ }
+ },
+ beforeUpload (file) {
+ if (file.type !== 'text/csv') {
+ return false
+ }
+
+ this.fileList = [file]
+ return false
+ },
+ handleSubmit (e) {
+ e.preventDefault()
+ this.form.validateFields((err, values) => {
+ if (err) {
+ return
+ }
+ const params = {}
+ for (const key in values) {
+ const input = values[key]
+ if (input === undefined) {
+ continue
+ }
+ if (key === 'file') {
+ continue
+ }
+
+ params[key] = input
+ }
+
+ if (this.fileList.length !== 1) {
+ return
+ }
+
+ var rules = this.rulesCsvToJson(this.rulesCsv)
+ rules.forEach(function (values, index) {
+ for (const key in values) {
+ params['rules[' + index + '].' + key] = values[key]
+ }
+ })
+
+ this.importRole(params)
+ })
+ },
+ closeAction () {
+ this.$emit('close-action')
+ },
+ importRole (params) {
+ this.loading = true
+ api('importRole', {}, 'POST', params).then(json => {
+ const role = json.importroleresponse.role
+ if (role) {
+ this.$emit('refresh-data')
+ this.$notification.success({
+ message: 'Import Role',
+ description: 'Sucessfully imported role ' + params.name
+ })
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.loading = false
+ this.closeAction()
+ })
+ },
+ rulesCsvToJson (rulesCsv) {
+ const columnDelimiter = ','
+ const lineDelimiter = '\n'
+ var lines = rulesCsv.split(lineDelimiter)
+ var result = []
+ if (lines.length === 0) {
+ return result
+ }
+ var headers = lines[0].split(columnDelimiter)
+ lines = lines.slice(1) // Remove header
+
+ lines.map((line, indexLine) => {
+ if (line.trim() === '') return // Empty line
+ var obj = {}
+ var currentline = line.trim().split(columnDelimiter)
+
+ headers.map((header, indexHeader) => {
+ if (indexHeader === 2 && currentline.length > 3) {
+ if (currentline[indexHeader].startsWith('"')) {
+ obj[header.trim()] = currentline[indexHeader].substr(1)
+ } else {
+ obj[header.trim()] = currentline[indexHeader]
+ }
+
+ for (let i = 3; i < currentline.length - 1; i++) {
+ obj[header.trim()] += columnDelimiter + currentline[i]
+ }
+
+ var lastColumn = currentline[currentline.length - 1]
+ if (lastColumn.endsWith('"')) {
+ obj[header.trim()] += columnDelimiter + lastColumn.substr(0, lastColumn.length - 1)
+ } else {
+ obj[header.trim()] += columnDelimiter + lastColumn
+ }
+ } else {
+ obj[header.trim()] = currentline[indexHeader]
+ }
+ })
+ result.push(obj)
+ })
+ return result
+ },
+ checkCsvRulesFile (rule, value, callback) {
+ if (!value || value === '' || value.file === '') {
+ callback()
+ } else {
+ if (value.file.type !== 'text/csv') {
+ callback(rule.message)
+ }
+
+ this.readCsvFile(value.file).then((validFile) => {
+ if (!validFile) {
+ callback(rule.message)
+ } else {
+ callback()
+ }
+ }).catch((reason) => {
+ console.log(reason)
+ callback(rule.message)
+ })
+ }
+ },
+ readCsvFile (file) {
+ return new Promise((resolve, reject) => {
+ if (window.FileReader) {
+ var reader = new FileReader()
+ reader.onload = (event) => {
+ this.rulesCsv = event.target.result
+ var lines = this.rulesCsv.split('\n')
+ var headers = lines[0].split(',')
+ if (headers.length !== 3) {
+ resolve(false)
+ } else if (!(headers[0].trim() === 'rule' && headers[1].trim() === 'permission' && headers[2].trim() === 'description')) {
+ resolve(false)
+ } else {
+ resolve(true)
+ }
+ }
+
+ reader.onerror = (event) => {
+ if (event.target.error.name === 'NotReadableError') {
+ reject(event.target.error)
+ }
+ }
+
+ reader.readAsText(file)
+ } else {
+ reject(this.$t('label.error.file.read'))
+ }
+ })
+ }
+ }
+}
+</script>
+
+<style scoped lang="less">
+ .form-layout {
+ width: 80vw;
+
+ @media (min-width: 700px) {
+ width: 550px;
+ }
+ }
+
+ .action-button {
+ text-align: right;
+
+ button {
+ margin-right: 5px;
+ }
+ }
+</style>
diff --git a/src/views/iam/RolePermissionTab.vue b/src/views/iam/RolePermissionTab.vue
index f95c0d6..bb63b9b 100644
--- a/src/views/iam/RolePermissionTab.vue
+++ b/src/views/iam/RolePermissionTab.vue
@@ -18,6 +18,11 @@
<template>
<a-icon v-if="loadingTable" type="loading" class="main-loading-spinner"></a-icon>
<div v-else>
+ <div style="width: 100%; display: flex; margin-bottom: 10px">
+ <a-button type="dashed" @click="exportRolePermissions" style="width: 100%" icon="download">
+ Export Rules
+ </a-button>
+ </div>
<div v-if="updateTable" class="loading-overlay">
<a-icon type="loading" />
</div>
@@ -166,7 +171,7 @@ export default {
api('listRolePermissions', { roleid: this.resource.id }).then(response => {
this.rules = response.listrolepermissionsresponse.rolepermission
}).catch(error => {
- console.error(error)
+ this.$notifyError(error)
}).finally(() => {
this.loadingTable = false
this.updateTable = false
@@ -178,7 +183,7 @@ export default {
roleid: this.resource.id,
ruleorder: this.rules.map(rule => rule.id)
}).catch(error => {
- console.error(error)
+ this.$notifyError(error)
}).finally(() => {
this.fetchData()
})
@@ -186,7 +191,7 @@ export default {
onRuleDelete (key) {
this.updateTable = true
api('deleteRolePermission', { id: key }).catch(error => {
- console.error(error)
+ this.$notifyError(error)
}).finally(() => {
this.fetchData()
})
@@ -204,7 +209,7 @@ export default {
}).then(() => {
this.fetchData()
}).catch(error => {
- console.error(error)
+ this.$notifyError(error)
})
},
onRuleSelect (value) {
@@ -224,11 +229,44 @@ export default {
roleid: this.resource.id
}).then(() => {
}).catch(error => {
- console.error(error)
+ this.$notifyError(error)
}).finally(() => {
this.resetNewFields()
this.fetchData()
})
+ },
+ rulesDataToCsv ({ data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
+ if (data === null || !data.length) {
+ return null
+ }
+
+ const keys = ['rule', 'permission', 'description']
+ let result = ''
+ result += keys.join(columnDelimiter)
+ result += lineDelimiter
+
+ data.forEach(item => {
+ keys.forEach(key => {
+ if (item[key] === undefined) {
+ item[key] = ''
+ }
+ result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
+ result += columnDelimiter
+ })
+ result = result.slice(0, -1)
+ result += lineDelimiter
+ })
+
+ return result
+ },
+ exportRolePermissions () {
+ const rulesCsvData = this.rulesDataToCsv({ data: this.rules })
+ const hiddenElement = document.createElement('a')
+ hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(rulesCsvData)
+ hiddenElement.target = '_blank'
+ hiddenElement.download = this.resource.name + '_' + this.resource.type + '.csv'
+ hiddenElement.click()
+ hiddenElement.delete()
}
}
}