diff options
author | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2020-02-12 22:30:49 +0300 |
---|---|---|
committer | Yoshie Muranaka <yoshiemuranaka@gmail.com> | 2020-02-22 00:32:15 +0300 |
commit | 183c27548046e12b94354aa598b5bcf956d31103 (patch) | |
tree | 4168e439d3554427648b7898a706d8a7aed84e10 | |
parent | c11d38945b8a51e4181142c2b8852ffcb30338d9 (diff) | |
download | webui-vue-183c27548046e12b94354aa598b5bcf956d31103.tar.xz |
Add batch actions to local user table
- Create TableToolbar component for table batch actions
- Added Toast warning type and toast title message translations
- Update vue-i18n package to latest v8.15.3 to use improved
pluarlization features
Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I455beba4f56b8209b1201bbc5ff3f616e960d189
-rw-r--r-- | package-lock.json | 28 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/assets/styles/_form-components.scss | 3 | ||||
-rw-r--r-- | src/assets/styles/_table.scss | 7 | ||||
-rw-r--r-- | src/assets/styles/_toast.scss | 4 | ||||
-rw-r--r-- | src/components/Global/TableToolbar.vue | 119 | ||||
-rw-r--r-- | src/components/Mixins/BVTableSelectableMixin.js | 44 | ||||
-rw-r--r-- | src/components/Mixins/BVToastMixin.js | 19 | ||||
-rw-r--r-- | src/locales/en.json | 39 | ||||
-rw-r--r-- | src/store/api.js | 3 | ||||
-rw-r--r-- | src/store/modules/AccessControl/LocalUserMangementStore.js | 142 | ||||
-rw-r--r-- | src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue | 101 |
12 files changed, 475 insertions, 36 deletions
diff --git a/package-lock.json b/package-lock.json index d07b4e7d..63763cb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7198,10 +7198,7 @@ "debug": { "version": "4.1.1", "resolved": false, - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==" }, "deep-extend": { "version": "0.6.0", @@ -7414,11 +7411,6 @@ "minimist": "0.0.8" } }, - "ms": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, "needle": { "version": "2.3.0", "resolved": false, @@ -7426,16 +7418,28 @@ "dev": true, "optional": true, "requires": { + "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "optional": true, "requires": { "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "optional": true + } } } } diff --git a/package.json b/package.json index 361adb2b..14a0a801 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "js-cookie": "^2.2.1", "vue": "2.6.11", "vue-date-fns": "1.1.0", - "vue-i18n": "8.0.0", + "vue-i18n": "8.15.3", "vue-router": "3.1.3", "vuelidate": "^0.7.4", "vuex": "3.0.1" diff --git a/src/assets/styles/_form-components.scss b/src/assets/styles/_form-components.scss index 7194d9e8..89abfb3f 100644 --- a/src/assets/styles/_form-components.scss +++ b/src/assets/styles/_form-components.scss @@ -20,9 +20,6 @@ color: $gray-900 !important; font-size: 16px; border-color: $gray-400 !important; - &::before { - border-color: $primary; - } &.is-invalid, &:invalid { border-bottom: 2px solid $danger !important; diff --git a/src/assets/styles/_table.scss b/src/assets/styles/_table.scss index ff1ed302..7d265c8e 100644 --- a/src/assets/styles/_table.scss +++ b/src/assets/styles/_table.scss @@ -1,3 +1,8 @@ +table { + position: relative; + z-index: $zindex-dropdown; +} + .table-light { td { border-top: none; @@ -18,4 +23,4 @@ padding-top: 0; padding-bottom: 0; } -} +}
\ No newline at end of file diff --git a/src/assets/styles/_toast.scss b/src/assets/styles/_toast.scss index 3f2f08c0..538f9968 100644 --- a/src/assets/styles/_toast.scss +++ b/src/assets/styles/_toast.scss @@ -29,4 +29,8 @@ .b-toast-danger .toast { border-left-color: $danger!important; +} + +.b-toast-warning .toast { + border-left-color: $warning!important; }
\ No newline at end of file diff --git a/src/components/Global/TableToolbar.vue b/src/components/Global/TableToolbar.vue new file mode 100644 index 00000000..fc3736db --- /dev/null +++ b/src/components/Global/TableToolbar.vue @@ -0,0 +1,119 @@ +<template> + <transition name="slide"> + <div v-if="isToolbarActive" class="toolbar-container"> + <div class="toolbar-content"> + <p class="toolbar-selected"> + {{ selectedItemsCount }} {{ $t('global.actions.selected') }} + </p> + <div class="toolbar-actions d-flex"> + <b-button + v-for="(action, index) in actions" + :key="index" + variant="primary" + class="d-block" + @click="$emit('batchAction', action.value)" + > + {{ $t(action.labelKey) }} + </b-button> + <b-button + variant="primary" + class="d-block" + @click="$emit('clearSelected')" + > + {{ $t('global.actions.cancel') }} + </b-button> + </div> + </div> + </div> + </transition> +</template> + +<script> +export default { + name: 'TableToolbar', + props: { + selectedItemsCount: { + type: Number, + required: true + }, + actions: { + type: Array, + required: true, + validator: prop => { + return prop.every(action => { + return ( + action.hasOwnProperty('value') && action.hasOwnProperty('labelKey') + ); + }); + } + } + }, + data() { + return { + isToolbarActive: false + }; + }, + watch: { + selectedItemsCount: function(selectedItemsCount) { + if (selectedItemsCount > 0) { + this.isToolbarActive = true; + } else { + this.isToolbarActive = false; + } + } + } +}; +</script> + +<style lang="scss" scoped> +$toolbar-height: 46px; + +.toolbar-container { + width: 100%; + position: relative; +} + +.toolbar-content { + height: $toolbar-height; + background-color: $primary; + color: $white; + position: absolute; + left: 0; + right: 0; + top: -$toolbar-height; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.toolbar-actions { + > :last-child { + position: relative; + &::before { + content: ''; + position: absolute; + height: $toolbar-height / 2; + border-left: 2px solid $white; + left: -2px; + top: $toolbar-height / 4; + } + } +} + +.toolbar-selected { + line-height: $toolbar-height; + margin: 0; + padding: 0 $spacer; +} + +.slide-enter-active { + transition: transform $duration--moderate-02 $entrance-easing--productive; +} +.slide-leave-active { + transition: transform $duration--moderate-02 $exit-easing--productive; +} +.slide-enter, +.slide-leave-to { + transform: translateY($toolbar-height); +} +</style> diff --git a/src/components/Mixins/BVTableSelectableMixin.js b/src/components/Mixins/BVTableSelectableMixin.js new file mode 100644 index 00000000..fba2f2b8 --- /dev/null +++ b/src/components/Mixins/BVTableSelectableMixin.js @@ -0,0 +1,44 @@ +const BVTableSelectableMixin = { + data() { + return { + tableHeaderCheckboxModel: false, + tableHeaderCheckboxIndeterminate: false, + selectedRows: [] + }; + }, + methods: { + clearSelectedRows(tableRef) { + if (tableRef) tableRef.clearSelected(); + }, + toggleSelectRow(tableRef, rowIndex) { + if (tableRef && rowIndex !== undefined) { + tableRef.isRowSelected(rowIndex) + ? tableRef.unselectRow(rowIndex) + : tableRef.selectRow(rowIndex); + } + }, + onRowSelected(selectedRows, totalRowsCount) { + if (selectedRows && totalRowsCount !== undefined) { + this.selectedRows = selectedRows; + if (selectedRows.length === 0) { + this.tableHeaderCheckboxIndeterminate = false; + this.tableHeaderCheckboxModel = false; + } else if (selectedRows.length === totalRowsCount) { + this.tableHeaderCheckboxIndeterminate = false; + this.tableHeaderCheckboxModel = true; + } else { + this.tableHeaderCheckboxIndeterminate = true; + this.tableHeaderCheckboxModel = false; + } + } + }, + onChangeHeaderCheckbox(tableRef) { + if (tableRef) { + if (this.tableHeaderCheckboxModel) tableRef.clearSelected(); + else tableRef.selectAllRows(); + } + } + } +}; + +export default BVTableSelectableMixin; diff --git a/src/components/Mixins/BVToastMixin.js b/src/components/Mixins/BVToastMixin.js index 489173c9..a46f5e50 100644 --- a/src/components/Mixins/BVToastMixin.js +++ b/src/components/Mixins/BVToastMixin.js @@ -1,22 +1,33 @@ +import i18n from '../../i18n'; + const BVToastMixin = { methods: { - successToast(message) { + successToast(message, title = i18n.t('global.response.success')) { this.$root.$bvToast.toast(message, { - title: 'Success', + title, variant: 'success', autoHideDelay: 10000, //auto hide in milliseconds isStatus: true, solid: true }); }, - errorToast(message) { + errorToast(message, title = i18n.t('global.response.error')) { this.$root.$bvToast.toast(message, { - title: 'Error', + title, variant: 'danger', noAutoHide: true, isStatus: true, solid: true }); + }, + warningToast(message, title = i18n.t('global.response.warning')) { + this.$root.$bvToast.toast(message, { + title, + variant: 'warning', + noAutoHide: true, + isStatus: true, + solid: true + }); } } }; diff --git a/src/locales/en.json b/src/locales/en.json index 2b6fa07a..9d89ce60 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -6,12 +6,27 @@ "on": "on", "off": "off", "actions": { - "confirm": "Confirm" + "confirm": "Confirm", + "cancel": "Cancel", + "delete": "Delete", + "selected": "Selected" + }, + "response": { + "success": "Success", + "error": "Error", + "warning": "Warning" } }, "ariaLabels": { "showPassword": "Show password as plain text. Note: this will visually expose your password on the screen." }, + "pageTitle": { + "localUserMgmt": "Local user management", + "login": "Login", + "overview": "Overview", + "unauthorized": "Unauthorized", + "rebootBmc": "Reboot BMC" + }, "login": { "language": { "label": "Language" @@ -67,13 +82,6 @@ "solConsole": "Serial over LAN console" } }, - "pageTitle": { - "localUserMgmt": "Local user management", - "login": "Login", - "overview": "Overview", - "unauthorized": "Unauthorized", - "rebootBmc": "Reboot BMC" - }, "pageRebootBmc": { "rebootInformation": "When you reboot the BMC, your web browser loses contact with the BMC for several minutes. When the BMC is back online, you may need to log in again.", "rebootBmc": "Reboot BMC", @@ -85,5 +93,20 @@ "successRebootStart": "Rebooting BMC.", "errorRebootStart": "Error rebooting BMC." } + }, + "localUserManagement": { + "tableActions": { + "delete": "@:global.actions.delete", + "enable": "Enable", + "disable": "Disable" + }, + "toastMessages": { + "successDeleteUsers": "Successfully deleted %{count} user. | Successfully deleted %{count} users.", + "errorDeleteUsers": "Error deleting %{count} user. | Error deleting %{count} users.", + "successEnableUsers": "Successfully enabled %{count} user. | Successfully enabled %{count} users.", + "errorEnableUsers": "Error enabling %{count} user. | Error enabling %{count} users.", + "successDisableUsers": "Successfully disabled %{count} user. | Successfully disabled %{count} users.", + "errorDisableUsers": "Error disabling %{count} user. | Error disabling %{count} users." + } } }
\ No newline at end of file diff --git a/src/store/api.js b/src/store/api.js index 0f8c9484..8fdbdd2f 100644 --- a/src/store/api.js +++ b/src/store/api.js @@ -40,5 +40,8 @@ export default { }, all(promises) { return Axios.all(promises); + }, + spread(callback) { + return Axios.spread(callback); } }; diff --git a/src/store/modules/AccessControl/LocalUserMangementStore.js b/src/store/modules/AccessControl/LocalUserMangementStore.js index bc14c734..eb5822e1 100644 --- a/src/store/modules/AccessControl/LocalUserMangementStore.js +++ b/src/store/modules/AccessControl/LocalUserMangementStore.js @@ -1,4 +1,20 @@ import api from '../../api'; +import i18n from '../../../i18n'; + +const getResponseCount = responses => { + let successCount = 0; + let errorCount = 0; + + responses.forEach(response => { + if (response instanceof Error) errorCount++; + else successCount++; + }); + + return { + successCount, + errorCount + }; +}; const LocalUserManagementStore = { namespaced: true, @@ -73,6 +89,132 @@ const LocalUserManagementStore = { console.log(error); throw new Error(`Error deleting user '${username}'.`); }); + }, + async deleteUsers({ dispatch }, users) { + const promises = users.map(({ username }) => { + return api + .delete(`/redfish/v1/AccountService/Accounts/${username}`) + .catch(error => { + console.log(error); + return error; + }); + }); + return await api + .all(promises) + .then(response => { + dispatch('getUsers'); + return response; + }) + .then( + api.spread((...responses) => { + const { successCount, errorCount } = getResponseCount(responses); + let toastMessages = []; + + if (successCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.successDeleteUsers', + successCount + ); + toastMessages.push({ type: 'success', message }); + } + + if (errorCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.errorDeleteUsers', + errorCount + ); + toastMessages.push({ type: 'error', message }); + } + + return toastMessages; + }) + ); + }, + async enableUsers({ dispatch }, users) { + const data = { + Enabled: true + }; + const promises = users.map(({ username }) => { + return api + .patch(`/redfish/v1/AccountService/Accounts/${username}`, data) + .catch(error => { + console.log(error); + return error; + }); + }); + return await api + .all(promises) + .then(response => { + dispatch('getUsers'); + return response; + }) + .then( + api.spread((...responses) => { + const { successCount, errorCount } = getResponseCount(responses); + let toastMessages = []; + + if (successCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.successEnableUsers', + successCount + ); + toastMessages.push({ type: 'success', message }); + } + + if (errorCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.errorEnableUsers', + errorCount + ); + toastMessages.push({ type: 'error', message }); + } + + return toastMessages; + }) + ); + }, + async disableUsers({ dispatch }, users) { + const data = { + Enabled: false + }; + const promises = users.map(({ username }) => { + return api + .patch(`/redfish/v1/AccountService/Accounts/${username}`, data) + .catch(error => { + console.log(error); + return error; + }); + }); + return await api + .all(promises) + .then(response => { + dispatch('getUsers'); + return response; + }) + .then( + api.spread((...responses) => { + const { successCount, errorCount } = getResponseCount(responses); + let toastMessages = []; + + if (successCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.successDisableUsers', + successCount + ); + toastMessages.push({ type: 'success', message }); + } + + if (errorCount) { + const message = i18n.tc( + 'localUserManagement.toastMessages.errorDisableUsers', + errorCount + ); + toastMessages.push({ type: 'error', message }); + } + + return toastMessages; + }) + ); } } }; diff --git a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue index a5ba7ba0..d68c9534 100644 --- a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue +++ b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue @@ -15,7 +15,37 @@ </b-row> <b-row> <b-col xl="9"> - <b-table show-empty :fields="fields" :items="tableItems"> + <table-toolbar + ref="toolbar" + :selected-items-count="selectedRows.length" + :actions="tableToolbarActions" + @clearSelected="clearSelectedRows($refs.table)" + @batchAction="onBatchAction" + /> + <b-table + ref="table" + selectable + no-select-on-click + :fields="fields" + :items="tableItems" + @row-selected="onRowSelected($event, tableItems.length)" + > + <!-- Checkbox column --> + <template v-slot:head(checkbox)> + <b-form-checkbox + v-model="tableHeaderCheckboxModel" + :indeterminate="tableHeaderCheckboxIndeterminate" + @change="onChangeHeaderCheckbox($refs.table)" + /> + </template> + <template v-slot:cell(checkbox)="row"> + <b-form-checkbox + v-model="row.rowSelected" + @change="toggleSelectRow($refs.table, row.index)" + /> + </template> + + <!-- table actions column --> <template v-slot:cell(actions)="data"> <b-button aria-label="Edit user" @@ -63,10 +93,13 @@ import IconAdd from '@carbon/icons-vue/es/add--alt/20'; import IconSettings from '@carbon/icons-vue/es/settings/20'; import IconChevron from '@carbon/icons-vue/es/chevron--up/20'; -import TableRoles from './TableRoles'; import ModalUser from './ModalUser'; import ModalSettings from './ModalSettings'; import PageTitle from '../../../components/Global/PageTitle'; +import TableRoles from './TableRoles'; +import TableToolbar from '../../../components/Global/TableToolbar'; + +import BVTableSelectableMixin from '../../../components/Mixins/BVTableSelectableMixin'; import BVToastMixin from '../../../components/Mixins/BVToastMixin'; export default { @@ -79,15 +112,22 @@ export default { IconTrashcan, ModalSettings, ModalUser, + PageTitle, TableRoles, - PageTitle + TableToolbar }, - mixins: [BVToastMixin], + mixins: [BVTableSelectableMixin, BVToastMixin], data() { return { activeUser: null, settings: null, fields: [ + { + key: 'checkbox', + label: '', + tdClass: 'table-cell__checkbox' + }, + 'checkbox', 'username', 'privilege', 'status', @@ -96,6 +136,20 @@ export default { label: '', tdClass: 'table-cell__actions' } + ], + tableToolbarActions: [ + { + value: 'delete', + labelKey: 'localUserManagement.tableActions.delete' + }, + { + value: 'enable', + labelKey: 'localUserManagement.tableActions.enable' + }, + { + value: 'disable', + labelKey: 'localUserManagement.tableActions.disable' + } ] }; }, @@ -174,15 +228,48 @@ export default { .dispatch('localUsers/deleteUser', username) .then(success => this.successToast(success)) .catch(({ message }) => this.errorToast(message)); + }, + onBatchAction(action) { + switch (action) { + case 'delete': + this.$store + .dispatch('localUsers/deleteUsers', this.selectedRows) + .then(messages => { + messages.forEach(({ type, message }) => { + if (type === 'success') this.successToast(message); + if (type === 'error') this.errorToast(message); + }); + }); + break; + case 'enable': + this.$store + .dispatch('localUsers/enableUsers', this.selectedRows) + .then(messages => { + messages.forEach(({ type, message }) => { + if (type === 'success') this.successToast(message); + if (type === 'error') this.errorToast(message); + }); + }); + break; + case 'disable': + this.$store + .dispatch('localUsers/disableUsers', this.selectedRows) + .then(messages => { + messages.forEach(({ type, message }) => { + if (type === 'success') this.successToast(message); + if (type === 'error') this.errorToast(message); + }); + }); + break; + default: + break; + } } } }; </script> <style lang="scss" scoped> -h1 { - margin-bottom: 2rem; -} .btn.collapsed { svg { transform: rotate(180deg); |