summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYoshie Muranaka <yoshiemuranaka@gmail.com>2020-02-12 22:30:49 +0300
committerYoshie Muranaka <yoshiemuranaka@gmail.com>2020-02-22 00:32:15 +0300
commit183c27548046e12b94354aa598b5bcf956d31103 (patch)
tree4168e439d3554427648b7898a706d8a7aed84e10
parentc11d38945b8a51e4181142c2b8852ffcb30338d9 (diff)
downloadwebui-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.json28
-rw-r--r--package.json2
-rw-r--r--src/assets/styles/_form-components.scss3
-rw-r--r--src/assets/styles/_table.scss7
-rw-r--r--src/assets/styles/_toast.scss4
-rw-r--r--src/components/Global/TableToolbar.vue119
-rw-r--r--src/components/Mixins/BVTableSelectableMixin.js44
-rw-r--r--src/components/Mixins/BVToastMixin.js19
-rw-r--r--src/locales/en.json39
-rw-r--r--src/store/api.js3
-rw-r--r--src/store/modules/AccessControl/LocalUserMangementStore.js142
-rw-r--r--src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue101
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);