diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/locales/en-US.json | 4 | ||||
-rw-r--r-- | src/locales/ru-RU.json | 3 | ||||
-rw-r--r-- | src/store/api.js | 25 | ||||
-rw-r--r-- | src/store/modules/Authentication/AuthenticanStore.js | 53 | ||||
-rw-r--r-- | src/store/modules/Logs/EventLogStore.js | 16 | ||||
-rw-r--r-- | src/store/modules/Operations/FirmwareStore.js | 45 | ||||
-rw-r--r-- | src/views/HardwareStatus/Inventory/InventoryTableFans.vue | 41 | ||||
-rw-r--r-- | src/views/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue | 39 | ||||
-rw-r--r-- | src/views/Login/Login.vue | 5 | ||||
-rw-r--r-- | src/views/Logs/Dumps/Dumps.vue | 2 | ||||
-rw-r--r-- | src/views/Logs/EventLogs/EventLogs.vue | 20 | ||||
-rw-r--r-- | src/views/Operations/Firmware/FirmwareFormUpdate.vue | 78 | ||||
-rw-r--r-- | src/views/SecurityAndAccess/UserManagement/UserManagement.vue | 43 |
13 files changed, 242 insertions, 132 deletions
diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 44a63de3..bd820666 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -282,6 +282,7 @@ "errorLogStatusUpdate": "Error updating log status.", "errorResolveLogs": "Error resolving %{count} log. | Error resolving %{count} logs.", "errorUnresolveLogs": "Error unresolving %{count} log. | Error unresolving %{count} logs.", + "errorDownloadEventEntry": "Error download event log entry.", "successDelete": "Successfully deleted %{count} log. | Successfully deleted %{count} logs.", "successResolveLogs": "Successfully resolved %{count} log. | Successfully resolved %{count} logs.", "successUnresolveLogs": "Successfully unresolved %{count} log. | Successfully unresolved %{count} logs." @@ -347,7 +348,6 @@ "fileSource": "File source", "imageFile": "Image file", "startUpdate": "Start update", - "tftpServer": "TFTP server", "workstation": "Workstation" } }, @@ -554,6 +554,7 @@ "accountPolicySettings": "Account policy settings", "addUser": "Add user", "deleteUser": "Delete user | Delete users", + "disableUser": "Disable user | Disable users", "editUser": "Edit user", "viewPrivilegeRoleDescriptions": "View privilege role descriptions", "modal": { @@ -561,6 +562,7 @@ "accountStatus": "Account status", "automaticAfterTimeout": "Automatic after timeout", "batchDeleteConfirmMessage": "Are you sure you want to delete %{count} user? This action cannot be undone. | Are you sure you want to delete %{count} users? This action cannot be undone.", + "batchDisableConfirmMessage": "Are you sure you want to disable %{count} user? | Are you sure you want to disable %{count} users?", "cannotStartWithANumber": "Cannot start with a number", "clickSaveToUnlockAccount": "Click \"Save\" to unlock account", "confirmUserPassword": "Confirm user password", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index dcf7c597..4a6106de 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -347,7 +347,6 @@ "fileSource": "Источник файла", "imageFile": "Файл образа", "startUpdate": "Начать обновление", - "tftpServer": "TFTP сервер", "workstation": "Рабочая станция" } }, @@ -552,6 +551,7 @@ "accountPolicySettings": "Настройки политики учётной записи", "addUser": "Добавить пользователя", "deleteUser": "Удалить пользователя | Удалить пользователей", + "disableUser": "Отключить пользователя | Отключить пользователей", "editUser": "Редактировать пользователя", "viewPrivilegeRoleDescriptions": "Просмотр описаний привилегий ролей", "modal": { @@ -559,6 +559,7 @@ "accountStatus": "Статус учётной записи", "automaticAfterTimeout": "Автоматически после истечения таймаута", "batchDeleteConfirmMessage": "Вы уверены, что хотите удалить %{count} пользователя? Это действие нельзя отменить. | Вы уверены, что хотите удалить %{count} пользователей? Это действие нельзя отменить.", + "batchDisableConfirmMessage": "Вы уверены, что хотите отключить пользователя %{count}? | Вы уверены, что хотите отключить пользователей %{count}?", "cannotStartWithANumber": "Не может начинаться с цифры", "clickSaveToUnlockAccount": "Нажмите \"Сохранить\" для разблокировки учётной записи", "confirmUserPassword": "Подтвердите пароль пользователя", diff --git a/src/store/api.js b/src/store/api.js index 0bd84e62..664e2b76 100644 --- a/src/store/api.js +++ b/src/store/api.js @@ -1,4 +1,5 @@ import Axios from 'axios'; +import router from '../router'; import { setupCache, buildWebStorage } from 'axios-cache-interceptor'; //Do not change store import. @@ -36,11 +37,14 @@ api.interceptors.response.use(undefined, (error) => { } } + // Check if action is unauthorized. if (response.status == 403) { - // Check if action is unauthorized. - // Toast error message will appear on screen - // when the action is unauthorized. - store.commit('global/setUnauthorized'); + if (isPasswordExpired(response)) { + router.push('/change-password'); + } else { + // Toast error message will appear on screen. + store.commit('global/setUnauthorized'); + } } return Promise.reject(error); @@ -68,6 +72,9 @@ export default { spread(callback) { return Axios.spread(callback); }, + set_auth_token(token) { + axiosInstance.defaults.headers.common['X-Auth-Token'] = token; + }, }; export const getResponseCount = (responses) => { @@ -84,3 +91,13 @@ export const getResponseCount = (responses) => { errorCount, }; }; + +export const isPasswordExpired = (response) => { + let extInfoMsgs = response?.data?.['@Message.ExtendedInfo']; + return ( + extInfoMsgs && + extInfoMsgs.find( + (i) => i.MessageId.split('.')[4] === 'PasswordChangeRequired', + ) + ); +}; diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js index 2006661b..3122ab2f 100644 --- a/src/store/modules/Authentication/AuthenticanStore.js +++ b/src/store/modules/Authentication/AuthenticanStore.js @@ -1,4 +1,4 @@ -import api from '@/store/api'; +import api, { isPasswordExpired } from '@/store/api'; import Cookies from 'js-cookie'; import router from '@/router'; import { roles } from '@/router/routes'; @@ -10,21 +10,39 @@ const AuthenticationStore = { authError: false, xsrfCookie: Cookies.get('XSRF-TOKEN'), isAuthenticatedCookie: Cookies.get('IsAuthenticated'), + sessionURI: localStorage.getItem('sessionURI'), + xAuthToken: null, }, getters: { consoleWindow: (state) => state.consoleWindow, authError: (state) => state.authError, isLoggedIn: (state) => { + // We might have gotten XSRF-TOKEN (and HttpOnly SESSION cookie) by Mutual TLS authentication, + // without going through explicit Session creation return ( - state.xsrfCookie !== undefined || state.isAuthenticatedCookie == 'true' + state.xsrfCookie !== undefined || + state.isAuthenticatedCookie == 'true' || + state.xAuthToken !== null ); }, + // Used to authenticate WebSocket connections via subprotocol value token: (state) => state.xsrfCookie, }, mutations: { - authSuccess(state) { + authSuccess(state, { session, token }) { state.authError = false; state.xsrfCookie = Cookies.get('XSRF-TOKEN'); + // Preserve session data across page reloads and browser restarts + localStorage.setItem('sessionURI', session); + state.sessionURI = session; + // If we didn't get the XSRF cookie it means we are talking to a + // Redfish implementation that is not bmcweb. In this case get the token + // from headers and send it with the future requests, do not permanently + // save anywhere. + if (state.xsrfCookie === undefined) { + api.set_auth_token(token); + state.xAuthToken = token; + } }, authError(state, authError = true) { state.authError = authError; @@ -32,33 +50,40 @@ const AuthenticationStore = { logout(state) { Cookies.remove('XSRF-TOKEN'); Cookies.remove('IsAuthenticated'); + api.set_auth_token(undefined); localStorage.removeItem('storedUsername'); state.xsrfCookie = undefined; state.isAuthenticatedCookie = undefined; + localStorage.removeItem('sessionURI'); + state.sessionURI = null; + state.xAuthToken = null; + state.consoleWindow = false; }, - setConsoleWindow: (state, window) => (state.consoleWindow = window), }, actions: { login({ commit }, { username, password }) { commit('authError', false); return api - .post('/login', { - username: username, - password: password, + .post('/redfish/v1/SessionService/Sessions', { + UserName: username, + Password: password, + }) + .then((response) => { + commit('authSuccess', { + session: response.headers['location'], + token: response.headers['x-auth-token'], + }); + return isPasswordExpired(response); }) - .then(() => commit('authSuccess')) .catch((error) => { commit('authError'); throw new Error(error); }); }, - logout({ commit }) { + logout({ commit, state }) { api - .post('/logout', { data: [] }) - .then(() => { - commit('setConsoleWindow', false); - commit('logout'); - }) + .delete(state.sessionURI) + .then(() => commit('logout')) .then(() => router.push('/login')) .catch((error) => console.log(error)); }, diff --git a/src/store/modules/Logs/EventLogStore.js b/src/store/modules/Logs/EventLogStore.js index e67da39b..f302dffb 100644 --- a/src/store/modules/Logs/EventLogStore.js +++ b/src/store/modules/Logs/EventLogStore.js @@ -220,6 +220,22 @@ const EventLogStore = { throw new Error(i18n.t('pageEventLogs.toast.errorLogStatusUpdate')); }); }, + async downloadEntry(_, uri) { + return await api + .get(uri) + .then((response) => { + const blob = new Blob([response.data], { + type: response.headers['content-type'], + }); + return blob; + }) + .catch((error) => { + console.log(error); + throw new Error( + i18n.t('pageEventLogs.toast.errorDownloadEventEntry'), + ); + }); + }, }, }; diff --git a/src/store/modules/Operations/FirmwareStore.js b/src/store/modules/Operations/FirmwareStore.js index f6f965f9..6c216da8 100644 --- a/src/store/modules/Operations/FirmwareStore.js +++ b/src/store/modules/Operations/FirmwareStore.js @@ -9,11 +9,10 @@ const FirmwareStore = { bmcActiveFirmwareId: null, hostActiveFirmwareId: null, applyTime: null, + multipartHttpPushUri: null, httpPushUri: null, - tftpAvailable: false, }, getters: { - isTftpUploadAvailable: (state) => state.tftpAvailable, isSingleFileUploadEnabled: (state) => state.hostFirmware.length === 0, activeBmcFirmware: (state) => { return state.bmcFirmware.find( @@ -43,8 +42,8 @@ const FirmwareStore = { setHostFirmware: (state, firmware) => (state.hostFirmware = firmware), setApplyTime: (state, applyTime) => (state.applyTime = applyTime), setHttpPushUri: (state, httpPushUri) => (state.httpPushUri = httpPushUri), - setTftpUploadAvailable: (state, tftpAvailable) => - (state.tftpAvailable = tftpAvailable), + setMultipartHttpPushUri: (state, multipartHttpPushUri) => + (state.multipartHttpPushUri = multipartHttpPushUri), }, actions: { async getFirmwareInformation({ dispatch }) { @@ -111,20 +110,24 @@ const FirmwareStore = { .then(({ data }) => { const applyTime = data.HttpPushUriOptions.HttpPushUriApplyTime.ApplyTime; - const allowableActions = - data?.Actions?.['#UpdateService.SimpleUpdate']?.[ - 'TransferProtocol@Redfish.AllowableValues' - ]; commit('setApplyTime', applyTime); const httpPushUri = data.HttpPushUri; commit('setHttpPushUri', httpPushUri); - if (allowableActions?.includes('TFTP')) { - commit('setTftpUploadAvailable', true); - } + const multipartHttpPushUri = data.MultipartHttpPushUri; + commit('setMultipartHttpPushUri', multipartHttpPushUri); }) .catch((error) => console.log(error)); }, - async uploadFirmware({ state }, image) { + async uploadFirmware({ state, dispatch }, params) { + if (state.multipartHttpPushUri != null) { + return dispatch('uploadFirmwareMultipartHttpPush', params); + } else if (state.httpPushUri != null) { + return dispatch('uploadFirmwareHttpPush', params); + } else { + console.log('Do not support firmware push update'); + } + }, + async uploadFirmwareHttpPush({ state }, { image }) { return await api .post(state.httpPushUri, image, { headers: { 'Content-Type': 'application/octet-stream' }, @@ -134,16 +137,16 @@ const FirmwareStore = { throw new Error(i18n.t('pageFirmware.toast.errorUpdateFirmware')); }); }, - async uploadFirmwareTFTP(fileAddress) { - const data = { - TransferProtocol: 'TFTP', - ImageURI: fileAddress, - }; + async uploadFirmwareMultipartHttpPush({ state }, { image, targets }) { + const formData = new FormData(); + formData.append('UpdateFile', image); + let params = {}; + if (targets != null && targets.length > 0) params.Targets = targets; + formData.append('UpdateParameters', JSON.stringify(params)); return await api - .post( - '/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate', - data, - ) + .post(state.multipartHttpPushUri, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) .catch((error) => { console.log(error); throw new Error(i18n.t('pageFirmware.toast.errorUpdateFirmware')); diff --git a/src/views/HardwareStatus/Inventory/InventoryTableFans.vue b/src/views/HardwareStatus/Inventory/InventoryTableFans.vue index 62f0b76b..af4b461e 100644 --- a/src/views/HardwareStatus/Inventory/InventoryTableFans.vue +++ b/src/views/HardwareStatus/Inventory/InventoryTableFans.vue @@ -51,6 +51,12 @@ {{ value }} </template> + <!-- StatusState --> + <template #cell(statusState)="{ value }"> + <status-icon :status="statusStateIcon(value)" /> + {{ value }} + </template> + <template #row-details="{ item }"> <b-container fluid> <b-row> @@ -146,6 +152,12 @@ export default { tdClass: 'text-nowrap', }, { + key: 'statusState', + label: this.$t('pageInventory.table.state'), + formatter: this.dataFormatter, + tdClass: 'text-nowrap', + }, + { key: 'partNumber', label: this.$t('pageInventory.table.partNumber'), formatter: this.dataFormatter, @@ -183,11 +195,40 @@ export default { sortCompare(a, b, key) { if (key === 'health') { return this.sortStatus(a, b, key); + } else if (key === 'statusState') { + return this.sortStatusState(a, b, key); } }, onFiltered(filteredItems) { this.searchTotalFilteredRows = filteredItems.length; }, + /** + * Returns the appropriate icon based on the given status. + * + * @param {string} status - The status to determine the icon for. + * @return {string} The icon corresponding to the given status. + */ + statusStateIcon(status) { + switch (status) { + case 'Enabled': + return 'success'; + case 'Absent': + return 'warning'; + default: + return ''; + } + }, + /** + * Sorts the status state of two objects based on the provided key. + * + * @param {Object} a - The first object to compare. + * @param {Object} b - The second object to compare. + * @param {string} key - The key to use for comparison. + */ + sortStatusState(a, b, key) { + const statusState = ['Enabled', 'Absent']; + return statusState.indexOf(a[key]) - statusState.indexOf(b[key]); + }, }, }; </script> diff --git a/src/views/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue b/src/views/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue index df03fdf2..0ce8c823 100644 --- a/src/views/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue +++ b/src/views/HardwareStatus/Inventory/InventoryTablePowerSupplies.vue @@ -51,6 +51,12 @@ {{ value }} </template> + <!-- StatusState --> + <template #cell(statusState)="{ value }"> + <status-icon :status="statusStateIcon(value)" /> + {{ value }} + </template> + <template #row-details="{ item }"> <b-container fluid> <b-row> @@ -167,6 +173,12 @@ export default { tdClass: 'text-nowrap', }, { + key: 'statusState', + label: this.$t('pageInventory.table.state'), + formatter: this.dataFormatter, + tdClass: 'text-nowrap', + }, + { key: 'locationNumber', label: this.$t('pageInventory.table.locationNumber'), formatter: this.dataFormatter, @@ -204,11 +216,38 @@ export default { sortCompare(a, b, key) { if (key === 'health') { return this.sortStatus(a, b, key); + } else if (key === 'statusState') { + return this.sortStatusState(a, b, key); } }, onFiltered(filteredItems) { this.searchTotalFilteredRows = filteredItems.length; }, + /** + * Returns the icon to use for status state based on the given status. + * @param {string} status The status to determine the icon for. + * @return {string} The icon for the given status. + */ + statusStateIcon(status) { + switch (status) { + case 'Enabled': + return 'success'; + case 'Absent': + return 'warning'; + default: + return ''; + } + }, + /** + * Sorts the status state of two objects based on the provided key. + * @param {Object} a The first object to compare. + * @param {Object} b The second object to compare. + * @param {string} key The key to use for comparison. + */ + sortStatusState(a, b, key) { + const statusState = ['Enabled', 'Absent']; + return statusState.indexOf(a[key]) - statusState.indexOf(b[key]); + }, }, }; </script> diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue index db475c56..5e1e6ddd 100644 --- a/src/views/Login/Login.vue +++ b/src/views/Login/Login.vue @@ -117,14 +117,11 @@ export default { const password = this.userInfo.password; this.$store .dispatch('authentication/login', { username, password }) - .then(() => { + .then((PasswordChangeRequired) => { localStorage.setItem('storedLanguage', i18n.locale); localStorage.setItem('storedUsername', username); this.$store.commit('global/setUsername', username); this.$store.commit('global/setLanguagePreference', i18n.locale); - return this.$store.dispatch('authentication/getUserInfo', username); - }) - .then(({ PasswordChangeRequired }) => { if (PasswordChangeRequired) { this.$router.push('/change-password'); } else { diff --git a/src/views/Logs/Dumps/Dumps.vue b/src/views/Logs/Dumps/Dumps.vue index 75873a8c..e89acd93 100644 --- a/src/views/Logs/Dumps/Dumps.vue +++ b/src/views/Logs/Dumps/Dumps.vue @@ -306,7 +306,7 @@ export default { }, created() { this.startLoader(); - this.$store.dispatch('dumps/getBmcDumpEntries').finally(() => { + this.$store.dispatch('dumps/getAllDumps').finally(() => { this.endLoader(); this.isBusy = false; }); diff --git a/src/views/Logs/EventLogs/EventLogs.vue b/src/views/Logs/EventLogs/EventLogs.vue index 0e7c494e..b48bd441 100644 --- a/src/views/Logs/EventLogs/EventLogs.vue +++ b/src/views/Logs/EventLogs/EventLogs.vue @@ -151,11 +151,7 @@ </dl> </b-col> <b-col class="text-nowrap"> - <b-button - class="btn btn-secondary float-right" - :href="item.additionalDataUri" - target="_blank" - > + <b-button @click="downloadEntry(item.additionalDataUri)"> <icon-download />{{ $t('pageEventLogs.additionalDataUri') }} </b-button> </b-col> @@ -471,6 +467,20 @@ export default { }); }, methods: { + downloadEntry(uri) { + let filename = uri?.split('LogServices/')?.[1]; + filename.replace(RegExp('/', 'g'), '_'); + this.$store + .dispatch('eventLog/downloadEntry', uri) + .then((blob) => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); + URL.revokeObjectURL(link.href); + }) + .catch(({ message }) => this.errorToast(message)); + }, changelogStatus(row) { this.$store .dispatch('eventLog/updateEventLogStatus', { diff --git a/src/views/Operations/Firmware/FirmwareFormUpdate.vue b/src/views/Operations/Firmware/FirmwareFormUpdate.vue index ac4b23fc..28d1104d 100644 --- a/src/views/Operations/Firmware/FirmwareFormUpdate.vue +++ b/src/views/Operations/Firmware/FirmwareFormUpdate.vue @@ -2,21 +2,8 @@ <div> <div class="form-background p-3"> <b-form @submit.prevent="onSubmitUpload"> - <b-form-group - v-if="isTftpUploadAvailable" - :label="$t('pageFirmware.form.updateFirmware.fileSource')" - :disabled="isPageDisabled" - > - <b-form-radio v-model="isWorkstationSelected" :value="true"> - {{ $t('pageFirmware.form.updateFirmware.workstation') }} - </b-form-radio> - <b-form-radio v-model="isWorkstationSelected" :value="false"> - {{ $t('pageFirmware.form.updateFirmware.tftpServer') }} - </b-form-radio> - </b-form-group> - <!-- Workstation Upload --> - <template v-if="isWorkstationSelected"> + <template> <b-form-group :label="$t('pageFirmware.form.updateFirmware.imageFile')" label-for="image-file" @@ -37,25 +24,6 @@ </b-form-group> </template> - <!-- TFTP Server Upload --> - <template v-else> - <b-form-group - :label="$t('pageFirmware.form.updateFirmware.fileAddress')" - label-for="tftp-address" - > - <b-form-input - id="tftp-address" - v-model="tftpFileAddress" - type="text" - :state="getValidationState($v.tftpFileAddress)" - :disabled="isPageDisabled" - @input="$v.tftpFileAddress.$touch()" - /> - <b-form-invalid-feedback role="alert"> - {{ $t('global.form.fieldRequired') }} - </b-form-invalid-feedback> - </b-form-group> - </template> <b-btn data-test-id="firmware-button-startUpdate" type="submit" @@ -73,7 +41,7 @@ </template> <script> -import { requiredIf } from 'vuelidate/lib/validators'; +import { required } from 'vuelidate/lib/validators'; import BVToastMixin from '@/components/Mixins/BVToastMixin'; import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin'; @@ -99,36 +67,15 @@ export default { data() { return { loading, - isWorkstationSelected: true, file: null, - tftpFileAddress: null, isServerPowerOffRequired: process.env.VUE_APP_SERVER_OFF_REQUIRED === 'true', }; }, - computed: { - isTftpUploadAvailable() { - return this.$store.getters['firmware/isTftpUploadAvailable']; - }, - }, - watch: { - isWorkstationSelected: function () { - this.$v.$reset(); - this.file = null; - this.tftpFileAddress = null; - }, - }, validations() { return { file: { - required: requiredIf(function () { - return this.isWorkstationSelected; - }), - }, - tftpFileAddress: { - required: requiredIf(function () { - return !this.isWorkstationSelected; - }), + required, }, }; }, @@ -149,24 +96,13 @@ export default { title: this.$t('pageFirmware.toast.updateStarted'), timestamp: true, }); - if (this.isWorkstationSelected) { - this.dispatchWorkstationUpload(timerId); - } else { - this.dispatchTftpUpload(timerId); - } + this.dispatchWorkstationUpload(timerId); }, dispatchWorkstationUpload(timerId) { this.$store - .dispatch('firmware/uploadFirmware', this.file) - .catch(({ message }) => { - this.endLoader(); - this.errorToast(message); - clearTimeout(timerId); - }); - }, - dispatchTftpUpload(timerId) { - this.$store - .dispatch('firmware/uploadFirmwareTFTP', this.tftpFileAddress) + .dispatch('firmware/uploadFirmware', { + image: this.file, + }) .catch(({ message }) => { this.endLoader(); this.errorToast(message); diff --git a/src/views/SecurityAndAccess/UserManagement/UserManagement.vue b/src/views/SecurityAndAccess/UserManagement/UserManagement.vue index ab316688..944ea258 100644 --- a/src/views/SecurityAndAccess/UserManagement/UserManagement.vue +++ b/src/views/SecurityAndAccess/UserManagement/UserManagement.vue @@ -361,16 +361,39 @@ export default { .finally(() => this.endLoader()); break; case 'disable': - this.startLoader(); - this.$store - .dispatch('userManagement/disableUsers', this.selectedRows) - .then((messages) => { - messages.forEach(({ type, message }) => { - if (type === 'success') this.successToast(message); - if (type === 'error') this.errorToast(message); - }); - }) - .finally(() => this.endLoader()); + this.$bvModal + .msgBoxConfirm( + this.$tc( + 'pageUserManagement.modal.batchDisableConfirmMessage', + this.selectedRows.length, + ), + { + title: this.$tc( + 'pageUserManagement.disableUser', + this.selectedRows.length, + ), + okTitle: this.$tc( + 'pageUserManagement.disableUser', + this.selectedRows.length, + ), + cancelTitle: this.$t('global.action.cancel'), + autoFocusButton: 'ok', + }, + ) + .then((disableConfirmed) => { + if (disableConfirmed) { + this.startLoader(); + this.$store + .dispatch('userManagement/disableUsers', this.selectedRows) + .then((messages) => { + messages.forEach(({ type, message }) => { + if (type === 'success') this.successToast(message); + if (type === 'error') this.errorToast(message); + }); + }) + .finally(() => this.endLoader()); + } + }); break; } }, |