diff options
43 files changed, 1322 insertions, 291 deletions
diff --git a/.gitattributes b/.gitattributes index 94f480de..f702d2d9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -* text=auto eol=lf
\ No newline at end of file +* text=auto eol=lf +*.png binary +*.woff binary
\ No newline at end of file @@ -37,7 +37,7 @@ owners: reviewers: - a.nikhil@ibm.com -- Renuka.Sharanya.Pundla@ibm.com +- jwestover@nvidia.com - sivaprabug@ami.com matchers: @@ -46,15 +46,15 @@ openbmc: - name: Gunnar Mills email: gunnar@gmills.xyz discord: GunnarM +- name: Jason Westover + email: jwestover@nvidia.com + discord: jasonwestover - name: Kirankumar Ballapalli email: kirankumarb@ami.com discord: kirankumarb - name: Nikhil Ashoka email: a.nikhil@ibm.com discord: NikhilAshoka -- name: Renuka Sharanya Pundla - email: Renuka.Sharanya.Pundla@ibm.com - discord: Renuka Sharanya - name: Sivaprabu Ganesan email: sivaprabug@ami.com discord: sivaprabug diff --git a/src/components/Global/FormFile.vue b/src/components/Global/FormFile.vue index e18d6148..50ac9614 100644 --- a/src/components/Global/FormFile.vue +++ b/src/components/Global/FormFile.vue @@ -107,6 +107,7 @@ export default { display: flex; align-items: center; background-color: theme-color('light'); + word-break: break-all; // break long file name into multiple lines .btn { width: 36px; height: 36px; diff --git a/src/env/store/ibm.js b/src/env/store/ibm.js index 86fc52d1..4383948f 100644 --- a/src/env/store/ibm.js +++ b/src/env/store/ibm.js @@ -1,11 +1,8 @@ import store from '@/store'; -import DumpsStore from '@/store/modules/Logs/DumpsStore'; import KeyClearStore from '@/store/modules/Operations/KeyClearStore'; store.unregisterModule('virtualMedia'); -store.registerModule('dumps', DumpsStore); - store.registerModule('key-clear', KeyClearStore); export default store; diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 8ba7ac94..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", @@ -703,16 +705,21 @@ }, "pageNetwork": { "dhcp": "DHCP", + "dhcp6": "DHCPv6", "domainName": "domain name", "dns": "DNS server", "fqdn": "FQDN", "hostname": "Hostname", + "ipVersion": "Version of IP", "interfaceSection": "Interface settings", "ipv4": "IPv4", "ipv4Addresses": "IPv4 addresses", "ipv6": "IPv6", + "ipv6Addresses": "IPv6 addresses", "linkStatus": "Link status", "macAddress": "MAC address", + "gateway": "Gateway", + "ipv6DefaultGateway": "IPv6 Default Gateway", "network": "network", "networkSettings": "Network settings", "ntp": "NTP server", @@ -728,7 +735,9 @@ "dhcpConfirmTitle": "%{dhcpState} DHCP", "editHostnameTitle": "Edit hostname", "editMacAddressTitle": "Edit MAC address", + "editIPv6DefaultGatewayTitle": "Edit IPv6 Default Gateway", "gateway": "Gateway", + "prefixLength": "Prefix Length", "ipAddress": "IP address", "staticDns": "Static DNS", "subnetMask": "Subnet mask" @@ -736,11 +745,15 @@ "table": { "addDnsAddress": "Add IP address", "addIpv4Address": "Add static IPv4 address", + "addIpv6Address": "Add static IPv6 address", "addressOrigin": "Address origin", + "prefixLength": "Prefix Length", "deleteDns": "Delete DNS address", "deleteIpv4": "Delete IPv4 address", + "deleteIpv6": "Delete IPv6 address", "editDns": "Edit DNS address", "editIpv4": "Edit IPv4 address", + "editIpv6": "Edit IPv6 address", "gateway": "Gateway", "ipAddress": "IP address", "subnet": "Subnet mask" 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/GlobalStore.js b/src/store/modules/GlobalStore.js index 036dc481..10d50b1a 100644 --- a/src/store/modules/GlobalStore.js +++ b/src/store/modules/GlobalStore.js @@ -77,9 +77,29 @@ const GlobalStore = { }, }, actions: { + async getBmcPath() { + const serviceRoot = await api + .get('/redfish/v1') + .catch((error) => console.log(error)); + let bmcPath = serviceRoot.data?.ManagerProvidingService?.['@odata.id']; + if (!bmcPath) { + const managers = await api + .get('/redfish/v1/Managers') + .catch((error) => console.log(error)); + bmcPath = managers.data?.Members?.[0]?.['@odata.id']; + } + return bmcPath; + }, + async getSystemPath() { + const systems = await api + .get('/redfish/v1/Systems') + .catch((error) => console.log(error)); + let systemPath = systems.data?.Members?.[0]?.['@odata.id']; + return systemPath; + }, async getBmcTime({ commit }) { return await api - .get('/redfish/v1/Managers/bmc') + .get(`${await this.dispatch('global/getBmcPath')}`) .then((response) => { const bmcDateTime = response.data.DateTime; const date = new Date(bmcDateTime); @@ -87,9 +107,9 @@ const GlobalStore = { }) .catch((error) => console.log(error)); }, - getSystemInfo({ commit }) { + async getSystemInfo({ commit }) { api - .get('/redfish/v1/Systems/system') + .get(`${await this.dispatch('global/getSystemPath')}`) .then( ({ data: { diff --git a/src/store/modules/HardwareStatus/BmcStore.js b/src/store/modules/HardwareStatus/BmcStore.js index d96926ea..f0e4cf96 100644 --- a/src/store/modules/HardwareStatus/BmcStore.js +++ b/src/store/modules/HardwareStatus/BmcStore.js @@ -47,7 +47,7 @@ const BmcStore = { actions: { async getBmcInfo({ commit }) { return await api - .get('/redfish/v1/Managers/bmc') + .get(`${await this.dispatch('global/getBmcPath')}`) .then(({ data }) => commit('setBmcInfo', data)) .catch((error) => console.log(error)); }, diff --git a/src/store/modules/HardwareStatus/MemoryStore.js b/src/store/modules/HardwareStatus/MemoryStore.js index 787a0502..d9a107d3 100644 --- a/src/store/modules/HardwareStatus/MemoryStore.js +++ b/src/store/modules/HardwareStatus/MemoryStore.js @@ -60,7 +60,7 @@ const MemoryStore = { actions: { async getDimms({ commit }) { return await api - .get('/redfish/v1/Systems/system/Memory') + .get(`${await this.dispatch('global/getSystemPath')}/Memory`) .then(({ data: { Members } }) => { const promises = Members.map((item) => api.get(item['@odata.id'])); return api.all(promises); diff --git a/src/store/modules/HardwareStatus/ProcessorStore.js b/src/store/modules/HardwareStatus/ProcessorStore.js index 49f96208..446fdb9c 100644 --- a/src/store/modules/HardwareStatus/ProcessorStore.js +++ b/src/store/modules/HardwareStatus/ProcessorStore.js @@ -63,7 +63,7 @@ const ProcessorStore = { actions: { async getProcessorsInfo({ commit }) { return await api - .get('/redfish/v1/Systems/system/Processors') + .get(`${await this.dispatch('global/getSystemPath')}/Processors`) .then(({ data: { Members = [] } }) => Members.map((member) => api.get(member['@odata.id'])), ) diff --git a/src/store/modules/HardwareStatus/ServerLedStore.js b/src/store/modules/HardwareStatus/ServerLedStore.js index af228022..d4af0648 100644 --- a/src/store/modules/HardwareStatus/ServerLedStore.js +++ b/src/store/modules/HardwareStatus/ServerLedStore.js @@ -17,7 +17,7 @@ const ServerLedStore = { actions: { async getIndicatorLedActiveState({ commit }) { return await api - .get('/redfish/v1/Systems/system') + .get(`${await this.dispatch('global/getSystemPath')}`) .then((response) => { commit( 'setIndicatorLedActiveState', @@ -29,7 +29,7 @@ const ServerLedStore = { async saveIndicatorLedActiveState({ commit }, payload) { commit('setIndicatorLedActiveState', payload); return await api - .patch('/redfish/v1/Systems/system', { + .patch(`${await this.dispatch('global/getSystemPath')}`, { LocationIndicatorActive: payload, }) .catch((error) => { diff --git a/src/store/modules/HardwareStatus/SystemStore.js b/src/store/modules/HardwareStatus/SystemStore.js index ea519d73..87d2810b 100644 --- a/src/store/modules/HardwareStatus/SystemStore.js +++ b/src/store/modules/HardwareStatus/SystemStore.js @@ -37,16 +37,13 @@ const SystemStore = { actions: { async getSystem({ commit }) { return await api - .get('/redfish/v1') - .then((response) => - api.get(`${response.data.Systems['@odata.id']}/system`), - ) + .get(`${await this.dispatch('global/getSystemPath')}`) .then(({ data }) => commit('setSystemInfo', data)) .catch((error) => console.log(error)); }, async changeIdentifyLedState({ commit }, ledState) { return await api - .patch('/redfish/v1/Systems/system', { + .patch(`${await this.dispatch('global/getSystemPath')}`, { LocationIndicatorActive: ledState, }) .then(() => { diff --git a/src/store/modules/Logs/DumpsStore.js b/src/store/modules/Logs/DumpsStore.js index 328e3164..9391e571 100644 --- a/src/store/modules/Logs/DumpsStore.js +++ b/src/store/modules/Logs/DumpsStore.js @@ -24,9 +24,7 @@ const DumpsStore = { actions: { async getBmcDumpEntries() { return api - .get('/redfish/v1/') - .then((response) => api.get(response.data.Managers['@odata.id'])) - .then((response) => api.get(`${response.data['@odata.id']}/bmc`)) + .get(`${await this.dispatch('global/getBmcPath')}`) .then((response) => api.get(response.data.LogServices['@odata.id'])) .then((response) => api.get(`${response.data['@odata.id']}/Dump`)) .then((response) => api.get(response.data.Entries['@odata.id'])) @@ -34,9 +32,7 @@ const DumpsStore = { }, async getSystemDumpEntries() { return api - .get('/redfish/v1/') - .then((response) => api.get(response.data.Systems['@odata.id'])) - .then((response) => api.get(`${response.data['@odata.id']}/system`)) + .get(`${await this.dispatch('global/getSystemPath')}`) .then((response) => api.get(response.data.LogServices['@odata.id'])) .then((response) => api.get(`${response.data['@odata.id']}/Dump`)) .then((response) => api.get(response.data.Entries['@odata.id'])) @@ -56,7 +52,7 @@ const DumpsStore = { async createBmcDump() { return await api .post( - '/redfish/v1/Managers/bmc/LogServices/Dump/Actions/LogService.CollectDiagnosticData', + `${await this.dispatch('global/getBmcPath')}/LogServices/Dump/Actions/LogService.CollectDiagnosticData`, { DiagnosticDataType: 'Manager', OEMDiagnosticDataType: '', @@ -70,7 +66,7 @@ const DumpsStore = { async createSystemDump() { return await api .post( - '/redfish/v1/Systems/system/LogServices/Dump/Actions/LogService.CollectDiagnosticData', + `${await this.dispatch('global/getSystemPath')}/LogServices/Dump/Actions/LogService.CollectDiagnosticData`, { DiagnosticDataType: 'OEM', OEMDiagnosticDataType: 'System', @@ -123,7 +119,7 @@ const DumpsStore = { const totalDumpCount = state.allDumps.length; return await api .post( - '/redfish/v1/Managers/bmc/LogServices/Dump/Actions/LogService.ClearLog', + `${await this.dispatch('global/getBmcPath')}/LogServices/Dump/Actions/LogService.ClearLog`, ) .then(() => { commit('setAllDumps', []); diff --git a/src/store/modules/Logs/EventLogStore.js b/src/store/modules/Logs/EventLogStore.js index f7b2ead6..f302dffb 100644 --- a/src/store/modules/Logs/EventLogStore.js +++ b/src/store/modules/Logs/EventLogStore.js @@ -42,7 +42,9 @@ const EventLogStore = { actions: { async getEventLogData({ commit }) { return await api - .get('/redfish/v1/Systems/system/LogServices/EventLog/Entries') + .get( + `${await this.dispatch('global/getSystemPath')}/LogServices/EventLog/Entries`, + ) .then(({ data: { Members = [] } = {} }) => { const eventLogs = Members.map((log) => { const { @@ -79,7 +81,7 @@ const EventLogStore = { async deleteAllEventLogs({ dispatch }, data) { return await api .post( - '/redfish/v1/Systems/system/LogServices/EventLog/Actions/LogService.ClearLog', + `${await this.dispatch('global/getSystemPath')}/LogServices/EventLog/Actions/LogService.ClearLog`, ) .then(() => dispatch('getEventLogData')) .then(() => i18n.tc('pageEventLogs.toast.successDelete', data.length)) @@ -218,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/Logs/PostCodeLogsStore.js b/src/store/modules/Logs/PostCodeLogsStore.js index 7648b13c..7bd1410f 100644 --- a/src/store/modules/Logs/PostCodeLogsStore.js +++ b/src/store/modules/Logs/PostCodeLogsStore.js @@ -16,7 +16,9 @@ const PostCodeLogsStore = { actions: { async getPostCodesLogData({ commit }) { return await api - .get('/redfish/v1/Systems/system/LogServices/PostCodes/Entries') + .get( + `${await this.dispatch('global/getSystemPath')}/LogServices/PostCodes/Entries`, + ) .then(({ data: { Members = [] } = {} }) => { const postCodeLogs = Members.map((log) => { const { Created, MessageArgs, AdditionalDataURI } = log; @@ -37,7 +39,7 @@ const PostCodeLogsStore = { async deleteAllPostCodeLogs({ dispatch }, data) { return await api .post( - '/redfish/v1/Systems/system/LogServices/PostCodes/Actions/LogService.ClearLog', + `${await this.dispatch('global/getSystemPath')}/LogServices/PostCodes/Actions/LogService.ClearLog`, ) .then(() => dispatch('getPostCodesLogData')) .then(() => diff --git a/src/store/modules/Operations/BootSettingsStore.js b/src/store/modules/Operations/BootSettingsStore.js index 1f5a628f..89598456 100644 --- a/src/store/modules/Operations/BootSettingsStore.js +++ b/src/store/modules/Operations/BootSettingsStore.js @@ -32,7 +32,7 @@ const BootSettingsStore = { actions: { async getBootSettings({ commit }) { return await api - .get('/redfish/v1/Systems/system') + .get(`${await this.dispatch('global/getSystemPath')}`) .then(({ data: { Boot } }) => { commit( 'setBootSourceOptions', @@ -43,7 +43,10 @@ const BootSettingsStore = { }) .catch((error) => console.log(error)); }, - saveBootSettings({ commit, dispatch }, { bootSource, overrideEnabled }) { + async saveBootSettings( + { commit, dispatch }, + { bootSource, overrideEnabled }, + ) { const data = { Boot: {} }; data.Boot.BootSourceOverrideTarget = bootSource; @@ -56,7 +59,7 @@ const BootSettingsStore = { } return api - .patch('/redfish/v1/Systems/system', data) + .patch(`${await this.dispatch('global/getSystemPath')}`, data) .then((response) => { // If request success, commit the values commit('setBootSource', data.Boot.BootSourceOverrideTarget); diff --git a/src/store/modules/Operations/ControlStore.js b/src/store/modules/Operations/ControlStore.js index e76063ba..320df6f9 100644 --- a/src/store/modules/Operations/ControlStore.js +++ b/src/store/modules/Operations/ControlStore.js @@ -51,7 +51,7 @@ const ControlStore = { actions: { async getLastPowerOperationTime({ commit }) { return await api - .get('/redfish/v1/Systems/system') + .get(`${await this.dispatch('global/getSystemPath')}`) .then((response) => { const lastReset = response.data.LastResetTime; if (lastReset) { @@ -61,9 +61,9 @@ const ControlStore = { }) .catch((error) => console.log(error)); }, - getLastBmcRebootTime({ commit }) { + async getLastBmcRebootTime({ commit }) { return api - .get('/redfish/v1/Managers/bmc') + .get(`${await this.dispatch('global/getBmcPath')}`) .then((response) => { const lastBmcReset = response.data.LastResetTime; const lastBmcRebootTime = new Date(lastBmcReset); @@ -71,11 +71,13 @@ const ControlStore = { }) .catch((error) => console.log(error)); }, - async rebootBmc({ dispatch }) { + async rebootBmc() { const data = { ResetType: 'GracefulRestart' }; return await api - .post('/redfish/v1/Managers/bmc/Actions/Manager.Reset', data) - .then(() => dispatch('getLastBmcRebootTime')) + .post( + `${await this.dispatch('global/getBmcPath')}/Actions/Manager.Reset`, + data, + ) .then(() => i18n.t('pageRebootBmc.toast.successRebootStart')) .catch((error) => { console.log(error); @@ -117,10 +119,13 @@ const ControlStore = { commit('setOperationInProgress', false); dispatch('getLastPowerOperationTime'); }, - serverPowerChange({ commit }, data) { + async serverPowerChange({ commit }, data) { commit('setOperationInProgress', true); api - .post('/redfish/v1/Systems/system/Actions/ComputerSystem.Reset', data) + .post( + `${await this.dispatch('global/getSystemPath')}/Actions/ComputerSystem.Reset`, + data, + ) .catch((error) => { console.log(error); commit('setOperationInProgress', false); diff --git a/src/store/modules/Operations/FactoryResetStore.js b/src/store/modules/Operations/FactoryResetStore.js index 395cae19..84a8f08a 100644 --- a/src/store/modules/Operations/FactoryResetStore.js +++ b/src/store/modules/Operations/FactoryResetStore.js @@ -6,9 +6,12 @@ const FactoryResetStore = { actions: { async resetToDefaults() { return await api - .post('/redfish/v1/Managers/bmc/Actions/Manager.ResetToDefaults', { - ResetType: 'ResetAll', - }) + .post( + `${await this.dispatch('global/getBmcPath')}/Actions/Manager.ResetToDefaults`, + { + ResetType: 'ResetAll', + }, + ) .then(() => i18n.t('pageFactoryReset.toast.resetToDefaultsSuccess')) .catch((error) => { console.log('Factory Reset: ', error); @@ -19,7 +22,9 @@ const FactoryResetStore = { }, async resetBios() { return await api - .post('/redfish/v1/Systems/system/Bios/Actions/Bios.ResetBios') + .post( + `${await this.dispatch('global/getSystemPath')}/Bios/Actions/Bios.ResetBios`, + ) .then(() => i18n.t('pageFactoryReset.toast.resetBiosSuccess')) .catch((error) => { console.log('Factory Reset: ', error); diff --git a/src/store/modules/Operations/FirmwareStore.js b/src/store/modules/Operations/FirmwareStore.js index 7dce2316..64bd640f 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 }) { @@ -52,18 +51,18 @@ const FirmwareStore = { dispatch('getActiveBmcFirmware'); return await dispatch('getFirmwareInventory'); }, - getActiveBmcFirmware({ commit }) { + async getActiveBmcFirmware({ commit }) { return api - .get('/redfish/v1/Managers/bmc') + .get(`${await this.dispatch('global/getBmcPath')}`) .then(({ data: { Links } }) => { const id = Links?.ActiveSoftwareImage['@odata.id'].split('/').pop(); commit('setActiveBmcFirmwareId', id); }) .catch((error) => console.log(error)); }, - getActiveHostFirmware({ commit }) { + async getActiveHostFirmware({ commit }) { return api - .get('/redfish/v1/Systems/system/Bios') + .get(`${await this.dispatch('global/getSystemPath')}/Bios`) .then(({ data: { Links } }) => { const id = Links?.ActiveSoftwareImage['@odata.id'].split('/').pop(); commit('setActiveHostFirmwareId', id); @@ -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,22 @@ 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; + } else { + // TODO: Should be OK to leave Targets out, remove this clause + // when bmcweb is updated + params.Targets = [`${await this.dispatch('global/getBmcPath')}`]; + } + 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')); @@ -159,7 +168,7 @@ const FirmwareStore = { }, }; return await api - .patch('/redfish/v1/Managers/bmc', data) + .patch(`${await this.dispatch('global/getBmcPath')}`, data) .catch((error) => { console.log(error); throw new Error(i18n.t('pageFirmware.toast.errorSwitchImages')); diff --git a/src/store/modules/Operations/KeyClearStore.js b/src/store/modules/Operations/KeyClearStore.js index 78804e75..9e5e875e 100644 --- a/src/store/modules/Operations/KeyClearStore.js +++ b/src/store/modules/Operations/KeyClearStore.js @@ -10,7 +10,7 @@ const KeyClearStore = { }; return await api .patch( - '/redfish/v1/Systems/system/Bios/Settings', + `${await this.dispatch('global/getSystemPath')}/Bios/Settings`, selectedKeyForClearing, ) .then(() => i18n.t('pageKeyClear.toast.selectedKeyClearedSuccess')) diff --git a/src/store/modules/Operations/VirtualMediaStore.js b/src/store/modules/Operations/VirtualMediaStore.js index 1d27e215..9688d9c6 100644 --- a/src/store/modules/Operations/VirtualMediaStore.js +++ b/src/store/modules/Operations/VirtualMediaStore.js @@ -49,7 +49,7 @@ const VirtualMediaStore = { } return await api - .get('/redfish/v1/Managers/bmc/VirtualMedia') + .get(`${await this.dispatch('global/getBmcPath')}/VirtualMedia`) .then((response) => response.data.Members.map( (virtualMedia) => virtualMedia['@odata.id'], @@ -95,7 +95,7 @@ const VirtualMediaStore = { async mountImage(_, { id, data }) { return await api .post( - `/redfish/v1/Managers/bmc/VirtualMedia/${id}/Actions/VirtualMedia.InsertMedia`, + `${await this.dispatch('global/getBmcPath')}/VirtualMedia/${id}/Actions/VirtualMedia.InsertMedia`, data, ) .catch((error) => { @@ -106,7 +106,7 @@ const VirtualMediaStore = { async unmountImage(_, id) { return await api .post( - `/redfish/v1/Managers/bmc/VirtualMedia/${id}/Actions/VirtualMedia.EjectMedia`, + `${await this.dispatch('global/getBmcPath')}/VirtualMedia/${id}/Actions/VirtualMedia.EjectMedia`, ) .catch((error) => { console.log('Unmount image:', error); diff --git a/src/store/modules/SecurityAndAccess/CertificatesStore.js b/src/store/modules/SecurityAndAccess/CertificatesStore.js index 666f5fd5..5c7c36d2 100644 --- a/src/store/modules/SecurityAndAccess/CertificatesStore.js +++ b/src/store/modules/SecurityAndAccess/CertificatesStore.js @@ -1,29 +1,8 @@ import api from '@/store/api'; import i18n from '@/i18n'; -export const CERTIFICATE_TYPES = [ - { - type: 'HTTPS Certificate', - location: '/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/', - label: i18n.t('pageCertificates.httpsCertificate'), - }, - { - type: 'LDAP Certificate', - location: '/redfish/v1/AccountService/LDAP/Certificates/', - label: i18n.t('pageCertificates.ldapCertificate'), - }, - { - type: 'TrustStore Certificate', - location: '/redfish/v1/Managers/bmc/Truststore/Certificates/', - // Web UI will show 'CA Certificate' instead of - // 'TrustStore Certificate' after user testing revealed - // the term 'TrustStore Certificate' wasn't recognized/was unfamilar - label: i18n.t('pageCertificates.caCertificate'), - }, -]; - -const getCertificateProp = (type, prop) => { - const certificate = CERTIFICATE_TYPES.find( +const getCertificateProp = (certificateTypes, type, prop) => { + const certificate = certificateTypes.find( (certificate) => certificate.type === type, ); return certificate ? certificate[prop] : null; @@ -34,10 +13,12 @@ const CertificatesStore = { state: { allCertificates: [], availableUploadTypes: [], + certificateTypes: [], }, getters: { allCertificates: (state) => state.allCertificates, availableUploadTypes: (state) => state.availableUploadTypes, + certificateTypes: (state) => state.certificateTypes, }, mutations: { setCertificates(state, certificates) { @@ -46,9 +27,40 @@ const CertificatesStore = { setAvailableUploadTypes(state, availableUploadTypes) { state.availableUploadTypes = availableUploadTypes; }, + setCertificateTypes(state, certificateTypes) { + state.certificateTypes = certificateTypes; + }, }, actions: { - async getCertificates({ commit }) { + async getCertificateTypes({ commit }) { + const certificateTypes = [ + { + type: 'HTTPS Certificate', + location: `${await this.dispatch( + 'global/getBmcPath', + )}/NetworkProtocol/HTTPS/Certificates/`, + label: i18n.t('pageCertificates.httpsCertificate'), + }, + { + type: 'LDAP Certificate', + location: '/redfish/v1/AccountService/LDAP/Certificates/', + label: i18n.t('pageCertificates.ldapCertificate'), + }, + { + type: 'TrustStore Certificate', + location: `${await this.dispatch( + 'global/getBmcPath', + )}/Truststore/Certificates/`, + // Web UI will show 'CA Certificate' instead of + // 'TrustStore Certificate' after user testing revealed + // the term 'TrustStore Certificate' wasn't recognized/was unfamilar + label: i18n.t('pageCertificates.caCertificate'), + }, + ]; + await commit('setCertificateTypes', certificateTypes); + }, + async getCertificates({ dispatch, getters, commit }) { + await dispatch('getCertificateTypes'); return await api .get('/redfish/v1/CertificateService/CertificateLocations') .then( @@ -75,14 +87,18 @@ const CertificatesStore = { return { type: Name, location: data['@odata.id'], - certificate: getCertificateProp(Name, 'label'), + certificate: getCertificateProp( + getters['certificateTypes'], + Name, + 'label', + ), issuedBy: Issuer.CommonName, issuedTo: Subject.CommonName, validFrom: new Date(ValidNotBefore), validUntil: new Date(ValidNotAfter), }; }); - const availableUploadTypes = CERTIFICATE_TYPES.filter( + const availableUploadTypes = getters['certificateTypes'].filter( ({ type }) => !certificates .map((certificate) => certificate.type) @@ -95,15 +111,23 @@ const CertificatesStore = { ); }); }, - async addNewCertificate({ dispatch }, { file, type }) { + async addNewCertificate({ dispatch, getters }, { file, type }) { return await api - .post(getCertificateProp(type, 'location'), file, { - headers: { 'Content-Type': 'application/x-pem-file' }, - }) + .post( + getCertificateProp(getters['certificateTypes'], type, 'location'), + file, + { + headers: { 'Content-Type': 'application/x-pem-file' }, + }, + ) .then(() => dispatch('getCertificates')) .then(() => i18n.t('pageCertificates.toast.successAddCertificate', { - certificate: getCertificateProp(type, 'label'), + certificate: getCertificateProp( + getters['certificateTypes'], + type, + 'label', + ), }), ) .catch((error) => { @@ -112,7 +136,7 @@ const CertificatesStore = { }); }, async replaceCertificate( - { dispatch }, + { dispatch, getters }, { certificateString, location, type }, ) { const data = {}; @@ -128,7 +152,11 @@ const CertificatesStore = { .then(() => dispatch('getCertificates')) .then(() => i18n.t('pageCertificates.toast.successReplaceCertificate', { - certificate: getCertificateProp(type, 'label'), + certificate: getCertificateProp( + getters['certificateTypes'], + type, + 'label', + ), }), ) .catch((error) => { @@ -138,13 +166,17 @@ const CertificatesStore = { ); }); }, - async deleteCertificate({ dispatch }, { type, location }) { + async deleteCertificate({ dispatch, getters }, { type, location }) { return await api .delete(location) .then(() => dispatch('getCertificates')) .then(() => i18n.t('pageCertificates.toast.successDeleteCertificate', { - certificate: getCertificateProp(type, 'label'), + certificate: getCertificateProp( + getters['certificateTypes'], + type, + 'label', + ), }), ) .catch((error) => { @@ -154,7 +186,7 @@ const CertificatesStore = { ); }); }, - async generateCsr(_, userData) { + async generateCsr({ getters }, userData) { const { certificateType, country, @@ -173,7 +205,11 @@ const CertificatesStore = { const data = {}; data.CertificateCollection = { - '@odata.id': getCertificateProp(certificateType, 'location'), + '@odata.id': getCertificateProp( + getters['certificateTypes'], + certificateType, + 'location', + ), }; data.Country = country; data.State = state; diff --git a/src/store/modules/SecurityAndAccess/PoliciesStore.js b/src/store/modules/SecurityAndAccess/PoliciesStore.js index e6bcfb96..f1e98b27 100644 --- a/src/store/modules/SecurityAndAccess/PoliciesStore.js +++ b/src/store/modules/SecurityAndAccess/PoliciesStore.js @@ -31,7 +31,7 @@ const PoliciesStore = { actions: { async getNetworkProtocolStatus({ commit }) { return await api - .get('/redfish/v1/Managers/bmc/NetworkProtocol') + .get(`${await this.dispatch('global/getBmcPath')}/NetworkProtocol`) .then((response) => { const sshProtocol = response.data.SSH.ProtocolEnabled; const ipmiProtocol = response.data.IPMI.ProtocolEnabled; @@ -42,7 +42,7 @@ const PoliciesStore = { }, async getBiosStatus({ commit }) { return await api - .get('/redfish/v1/Systems/system/Bios') + .get(`${await this.dispatch('global/getSystemPath')}/Bios`) .then((response) => { commit('setRtadEnabled', response.data.Attributes.pvm_rtad); commit('setVtpmEnabled', response.data.Attributes.pvm_vtpm); @@ -66,7 +66,10 @@ const PoliciesStore = { }, }; return await api - .patch('/redfish/v1/Managers/bmc/NetworkProtocol', ipmi) + .patch( + `${await this.dispatch('global/getBmcPath')}/NetworkProtocol`, + ipmi, + ) .then(() => { if (protocolEnabled) { return i18n.t('pagePolicies.toast.successIpmiEnabled'); @@ -92,7 +95,10 @@ const PoliciesStore = { }, }; return await api - .patch('/redfish/v1/Managers/bmc/NetworkProtocol', ssh) + .patch( + `${await this.dispatch('global/getBmcPath')}/NetworkProtocol`, + ssh, + ) .then(() => { if (protocolEnabled) { return i18n.t('pagePolicies.toast.successSshEnabled'); @@ -113,7 +119,7 @@ const PoliciesStore = { async saveRtadState({ commit }, updatedRtad) { commit('setRtadEnabled', updatedRtad); return await api - .patch('/redfish/v1/Systems/system/Bios/Settings', { + .patch(`${await this.dispatch('global/getSystemPath')}/Bios/Settings`, { Attributes: { pvm_rtad: updatedRtad, }, @@ -137,7 +143,7 @@ const PoliciesStore = { async saveVtpmState({ commit }, updatedVtpm) { commit('setVtpmEnabled', updatedVtpm); return await api - .patch('/redfish/v1/Systems/system/Bios/Settings', { + .patch(`${await this.dispatch('global/getSystemPath')}/Bios/Settings`, { Attributes: { pvm_vtpm: updatedVtpm, }, diff --git a/src/store/modules/Settings/DateTimeStore.js b/src/store/modules/Settings/DateTimeStore.js index 51b722a8..9d804a7e 100644 --- a/src/store/modules/Settings/DateTimeStore.js +++ b/src/store/modules/Settings/DateTimeStore.js @@ -19,7 +19,7 @@ const DateTimeStore = { actions: { async getNtpData({ commit }) { return await api - .get('/redfish/v1/Managers/bmc/NetworkProtocol') + .get(`${await this.dispatch('global/getBmcPath')}/NetworkProtocol`) .then((response) => { const ntpServers = response.data.NTP.NTPServers; const isNtpProtocolEnabled = response.data.NTP.ProtocolEnabled; @@ -40,7 +40,10 @@ const DateTimeStore = { ntpData.NTP.NTPServers = dateTimeForm.ntpServersArray; } return await api - .patch(`/redfish/v1/Managers/bmc/NetworkProtocol`, ntpData) + .patch( + `${await this.dispatch('global/getBmcPath')}/NetworkProtocol`, + ntpData, + ) .then(async () => { if (!dateTimeForm.ntpProtocolEnabled) { const dateTimeData = { @@ -58,9 +61,12 @@ const DateTimeStore = { */ const timeoutVal = state.isNtpProtocolEnabled ? 20000 : 0; return await new Promise((resolve, reject) => { - setTimeout(() => { + setTimeout(async () => { return api - .patch(`/redfish/v1/Managers/bmc`, dateTimeData) + .patch( + `${await this.dispatch('global/getBmcPath')}`, + dateTimeData, + ) .then(() => resolve()) .catch(() => reject()); }, timeoutVal); diff --git a/src/store/modules/Settings/NetworkStore.js b/src/store/modules/Settings/NetworkStore.js index 9b016030..a249d22b 100644 --- a/src/store/modules/Settings/NetworkStore.js +++ b/src/store/modules/Settings/NetworkStore.js @@ -29,29 +29,47 @@ const NetworkStore = { state.globalNetworkSettings = data.map(({ data }) => { const { DHCPv4, + DHCPv6, HostName, IPv4Addresses, IPv4StaticAddresses, + IPv6Addresses, + IPv6StaticAddresses, LinkStatus, MACAddress, + IPv6DefaultGateway, } = data; return { defaultGateway: IPv4StaticAddresses[0]?.Gateway, //First static gateway is the default gateway + ipv6DefaultGateway: IPv6DefaultGateway, dhcpAddress: IPv4Addresses.filter( (ipv4) => ipv4.AddressOrigin === 'DHCP', ), + dhcpv6Address: IPv6Addresses.filter( + (ipv6) => + ipv6.AddressOrigin === 'SLAAC' || ipv6.AddressOrigin === 'DHCPv6', + ), dhcpEnabled: DHCPv4.DHCPEnabled, + dhcp6Enabled: DHCPv6.OperatingMode, hostname: HostName, macAddress: MACAddress, linkStatus: LinkStatus, staticAddress: IPv4StaticAddresses[0]?.Address, // Display first static address on overview page + ipv6StaticAddress: IPv6StaticAddresses[0]?.Address, useDnsEnabled: DHCPv4.UseDNSServers, useDomainNameEnabled: DHCPv4.UseDomainName, useNtpEnabled: DHCPv4.UseNTPServers, + useDnsEnabledIpv6: DHCPv6.UseDNSServers, + useDomainNameEnabledIpv6: DHCPv6.UseDomainName, + useNtpEnabledIpv6: DHCPv6.UseNTPServers, }; }); }, setNtpState: (state, ntpState) => (state.ntpState = ntpState), + setDomainNameStateIpv6: (state, domainState) => + (state.domainStateIpv6 = domainState), + setDnsStateIpv6: (state, dnsState) => (state.dnsStateIpv6 = dnsState), + setNtpStateIpv6: (state, ntpState) => (state.ntpStateIpv6 = ntpState), setSelectedInterfaceId: (state, selectedInterfaceId) => (state.selectedInterfaceId = selectedInterfaceId), setSelectedInterfaceIndex: (state, selectedInterfaceIndex) => @@ -60,7 +78,7 @@ const NetworkStore = { actions: { async getEthernetData({ commit }) { return await api - .get('/redfish/v1/Managers/bmc/EthernetInterfaces') + .get(`${await this.dispatch('global/getBmcPath')}/EthernetInterfaces`) .then((response) => response.data.Members.map( (ethernetInterface) => ethernetInterface['@odata.id'], @@ -96,7 +114,7 @@ const NetworkStore = { }; return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, data, ) .then(dispatch('getEthernetData')) @@ -114,18 +132,54 @@ const NetworkStore = { ); }); }, - async saveDomainNameState({ commit, state }, domainState) { - commit('setDomainNameState', domainState); + async saveDhcp6EnabledState({ state, dispatch }, dhcpState) { const data = { - DHCPv4: { - UseDomainName: domainState, + DHCPv6: { + OperatingMode: dhcpState ? 'Enabled' : 'Disabled', }, }; + return api + .patch( + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, + data, + ) + .then(dispatch('getEthernetData')) + .then(() => { + return i18n.t('pageNetwork.toast.successSaveNetworkSettings', { + setting: i18n.t('pageNetwork.dhcp6'), + }); + }) + .catch((error) => { + console.log(error); + throw new Error( + i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { + setting: i18n.t('pageNetwork.dhcp6'), + }), + ); + }); + }, + async saveDomainNameState({ commit, state }, { domainState, ipVersion }) { + var data; + if (ipVersion === 'IPv4') { + commit('setDomainNameState', domainState); + data = { + DHCPv4: { + UseDomainName: domainState, + }, + }; + } else if (ipVersion === 'IPv6') { + commit('setDomainNameStateIpv6', domainState); + data = { + DHCPv6: { + UseDomainName: domainState, + }, + }; + } // Saving to the first interface automatically updates DHCPv4 and DHCPv6 // on all interfaces return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.firstInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.firstInterfaceId}`, data, ) .then(() => { @@ -135,7 +189,9 @@ const NetworkStore = { }) .catch((error) => { console.log(error); - commit('setDomainNameState', !domainState); + if (ipVersion === 'IPv4') commit('setDomainNameState', !domainState); + else if (ipVersion === 'IPv6') + commit('setDomainNameStateIpv6', !domainState); throw new Error( i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { setting: i18n.t('pageNetwork.domainName'), @@ -143,18 +199,28 @@ const NetworkStore = { ); }); }, - async saveDnsState({ commit, state }, dnsState) { - commit('setDnsState', dnsState); - const data = { - DHCPv4: { - UseDNSServers: dnsState, - }, - }; + async saveDnsState({ commit, state }, { dnsState, ipVersion }) { + var data; + if (ipVersion === 'IPv4') { + commit('setDnsState', dnsState); + data = { + DHCPv4: { + UseDNSServers: dnsState, + }, + }; + } else if (ipVersion === 'IPv6') { + commit('setDnsStateIpv6', dnsState); + data = { + DHCPv6: { + UseDNSServers: dnsState, + }, + }; + } // Saving to the first interface automatically updates DHCPv4 and DHCPv6 // on all interfaces return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.firstInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.firstInterfaceId}`, data, ) .then(() => { @@ -164,7 +230,8 @@ const NetworkStore = { }) .catch((error) => { console.log(error); - commit('setDnsState', !dnsState); + if (ipVersion === 'IPv4') commit('setDnsState', !dnsState); + else if (ipVersion === 'IPv6') commit('setDnsStateIpv6', !dnsState); throw new Error( i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { setting: i18n.t('pageNetwork.dns'), @@ -172,18 +239,28 @@ const NetworkStore = { ); }); }, - async saveNtpState({ commit, state }, ntpState) { - commit('setNtpState', ntpState); - const data = { - DHCPv4: { - UseNTPServers: ntpState, - }, - }; + async saveNtpState({ commit, state }, { ntpState, ipVersion }) { + var data; + if (ipVersion === 'IPv4') { + commit('setNtpState', ntpState); + data = { + DHCPv4: { + UseNTPServers: ntpState, + }, + }; + } else if (ipVersion === 'IPv6') { + commit('setNtpStateIpv6', ntpState); + data = { + DHCPv6: { + UseNTPServers: ntpState, + }, + }; + } // Saving to the first interface automatically updates DHCPv4 and DHCPv6 // on all interfaces return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.firstInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.firstInterfaceId}`, data, ) .then(() => { @@ -193,7 +270,8 @@ const NetworkStore = { }) .catch((error) => { console.log(error); - commit('setNtpState', !ntpState); + if (ipVersion === 'IPv4') commit('setNtpState', !ntpState); + else if (ipVersion === 'IPv6') commit('setNtpStateIpv6', !ntpState); throw new Error( i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { setting: i18n.t('pageNetwork.ntp'), @@ -221,7 +299,7 @@ const NetworkStore = { const newAddress = [ipv4Form]; return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, { IPv4StaticAddresses: originalAddresses.concat(newAddress) }, ) .then(dispatch('getEthernetData')) @@ -239,10 +317,41 @@ const NetworkStore = { ); }); }, + async saveIpv6Address({ dispatch, state }, ipv6Form) { + const originalAddresses = state.ethernetData[ + state.selectedInterfaceIndex + ].IPv6StaticAddresses.map((ipv6) => { + const { Address, PrefixLength } = ipv6; + return { + Address, + PrefixLength, + }; + }); + const newAddress = [ipv6Form]; + return api + .patch( + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, + { IPv6StaticAddresses: originalAddresses.concat(newAddress) }, + ) + .then(dispatch('getEthernetData')) + .then(() => { + return i18n.t('pageNetwork.toast.successSaveNetworkSettings', { + setting: i18n.t('pageNetwork.ipv6'), + }); + }) + .catch((error) => { + console.log(error); + throw new Error( + i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { + setting: i18n.t('pageNetwork.ipv6'), + }), + ); + }); + }, async editIpv4Address({ dispatch, state }, ipv4TableData) { return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, { IPv4StaticAddresses: ipv4TableData }, ) .then(dispatch('getEthernetData')) @@ -260,10 +369,31 @@ const NetworkStore = { ); }); }, + async editIpv6Address({ dispatch, state }, ipv6TableData) { + return api + .patch( + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, + { IPv6StaticAddresses: ipv6TableData }, + ) + .then(dispatch('getEthernetData')) + .then(() => { + return i18n.t('pageNetwork.toast.successSaveNetworkSettings', { + setting: i18n.t('pageNetwork.ipv6'), + }); + }) + .catch((error) => { + console.log(error); + throw new Error( + i18n.t('pageNetwork.toast.errorSaveNetworkSettings', { + setting: i18n.t('pageNetwork.ipv6'), + }), + ); + }); + }, async saveSettings({ state, dispatch }, interfaceSettingsForm) { return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, interfaceSettingsForm, ) .then(dispatch('getEthernetData')) @@ -288,7 +418,7 @@ const NetworkStore = { const newDnsArray = originalAddresses.concat(newAddress); return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, { StaticNameServers: newDnsArray }, ) .then(dispatch('getEthernetData')) @@ -309,7 +439,7 @@ const NetworkStore = { async editDnsAddress({ dispatch, state }, dnsTableData) { return api .patch( - `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`, + `${await this.dispatch('global/getBmcPath')}/EthernetInterfaces/${state.selectedInterfaceId}`, { StaticNameServers: dnsTableData }, ) .then(dispatch('getEthernetData')) diff --git a/src/store/modules/Settings/PowerPolicyStore.js b/src/store/modules/Settings/PowerPolicyStore.js index 3adaec8d..fc65381e 100644 --- a/src/store/modules/Settings/PowerPolicyStore.js +++ b/src/store/modules/Settings/PowerPolicyStore.js @@ -44,7 +44,7 @@ const PowerPolicyStore = { }, async getPowerRestoreCurrentPolicy({ commit }) { return await api - .get('/redfish/v1/Systems/system') + .get(`${await this.dispatch('global/getSystemPath')}`) .then(({ data: { PowerRestorePolicy } }) => { commit('setPowerRestoreCurrentPolicy', PowerRestorePolicy); }) @@ -54,7 +54,7 @@ const PowerPolicyStore = { const data = { PowerRestorePolicy: powerPolicy }; return await api - .patch('/redfish/v1/Systems/system', data) + .patch(`${await this.dispatch('global/getSystemPath')}`, data) .then(() => { dispatch('getPowerRestoreCurrentPolicy'); return i18n.t('pagePowerRestorePolicy.toast.successSaveSettings'); 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/Overview/OverviewDumps.vue b/src/views/Overview/OverviewDumps.vue index a2ae4e4e..6db5d287 100644 --- a/src/views/Overview/OverviewDumps.vue +++ b/src/views/Overview/OverviewDumps.vue @@ -34,7 +34,7 @@ export default { }, }, created() { - this.$store.dispatch('dumps/getBmcDumpEntries').finally(() => { + this.$store.dispatch('dumps/getAllDumps').finally(() => { this.$root.$emit('overview-dumps-complete'); }); }, diff --git a/src/views/SecurityAndAccess/Certificates/ModalGenerateCsr.vue b/src/views/SecurityAndAccess/Certificates/ModalGenerateCsr.vue index d9d65912..9f60d2ba 100644 --- a/src/views/SecurityAndAccess/Certificates/ModalGenerateCsr.vue +++ b/src/views/SecurityAndAccess/Certificates/ModalGenerateCsr.vue @@ -366,7 +366,6 @@ import IconCheckmark from '@carbon/icons-vue/es/checkmark/20'; import { required, requiredIf } from 'vuelidate/lib/validators'; import { COUNTRY_LIST } from './CsrCountryCodes'; -import { CERTIFICATE_TYPES } from '@/store/modules/SecurityAndAccess/CertificatesStore'; import BVToastMixin from '@/components/Mixins/BVToastMixin'; import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; @@ -391,14 +390,6 @@ export default { keyCurveId: null, keyBitLength: null, }, - certificateOptions: CERTIFICATE_TYPES.reduce((arr, cert) => { - if (cert.type === 'TrustStore Certificate') return arr; - arr.push({ - text: cert.label, - value: cert.type, - }); - return arr; - }, []), countryOptions: COUNTRY_LIST.map((country) => ({ text: country.label, value: country.code, @@ -410,6 +401,21 @@ export default { csrStringCopied: false, }; }, + computed: { + certificateTypes() { + return this.$store.getters['certificates/certificateTypes']; + }, + certificateOptions() { + return this.certificateTypes.reduce((arr, cert) => { + if (cert.type === 'TrustStore Certificate') return arr; + arr.push({ + text: cert.label, + value: cert.type, + }); + return arr; + }, []); + }, + }, validations: { form: { certificateType: { required }, diff --git a/src/views/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue b/src/views/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue index 6ea2561a..beacf575 100644 --- a/src/views/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue +++ b/src/views/SecurityAndAccess/Ldap/ModalAddRoleGroup.vue @@ -16,7 +16,7 @@ <template v-if="roleGroup !== null"> <dl class="mb-4"> <dt>{{ $t('pageLdap.modal.groupName') }}</dt> - <dd>{{ form.groupName }}</dd> + <dd style="word-break: break-all">{{ form.groupName }}</dd> </dl> </template> 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; } }, diff --git a/src/views/Settings/Network/ModalDefaultGateway.vue b/src/views/Settings/Network/ModalDefaultGateway.vue new file mode 100644 index 00000000..48c05c1d --- /dev/null +++ b/src/views/Settings/Network/ModalDefaultGateway.vue @@ -0,0 +1,114 @@ +<template> + <b-modal + id="modal-default-gateway" + ref="modal" + :title="$t('pageNetwork.modal.editIPv6DefaultGatewayTitle')" + @hidden="resetForm" + > + <b-form id="gateway-settings" @submit.prevent="handleSubmit"> + <b-row> + <b-col sm="6"> + <b-form-group + :label="$t('pageNetwork.gateway')" + label-for="defaultGateway" + > + <b-form-input + id="defaultGateway" + v-model.trim="form.defaultGateway" + data-test-id="network-input-gateway" + type="text" + :state="getValidationState($v.form.defaultGateway)" + @change="$v.form.defaultGateway.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <div v-if="!$v.form.defaultGateway.required"> + {{ $t('global.form.fieldRequired') }} + </div> + <div v-if="!$v.form.defaultGateway.validateGateway"> + {{ $t('global.form.invalidFormat') }} + </div> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + </b-row> + </b-form> + <template #modal-footer="{ cancel }"> + <b-button variant="secondary" @click="cancel()"> + {{ $t('global.action.cancel') }} + </b-button> + <b-button + form="gateway-settings" + type="submit" + variant="primary" + @click="onOk" + > + {{ $t('global.action.add') }} + </b-button> + </template> + </b-modal> +</template> + +<script> +import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; +import { required, helpers } from 'vuelidate/lib/validators'; + +const validateGateway = helpers.regex( + 'validateGateway', + /^((?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}|:(?::[a-fA-F0-9]{1,4}){1,7}|::|(?:[a-fA-F0-9]{1,4}:){6}(?:[0-9]{1,3}\.){3}[0-9]{1,3}|::(?:[a-fA-F0-9]{1,4}:){0,5}(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,5}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,4}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,2}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-fA-F0-9]{1,4}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|::(?:[0-9]{1,3}\.){3}[0-9]{1,3})$/, +); + +export default { + mixins: [VuelidateMixin], + props: { + defaultGateway: { + type: String, + default: '', + }, + }, + data() { + return { + form: { + defaultGateway: '', + }, + }; + }, + watch: { + defaultGateway() { + this.form.defaultGateway = this.defaultGateway; + }, + }, + validations() { + return { + form: { + defaultGateway: { + required, + validateGateway, + }, + }, + }; + }, + methods: { + handleSubmit() { + this.$v.$touch(); + if (this.$v.$invalid) return; + this.$emit('ok', { IPv6DefaultGateway: this.form.defaultGateway }); + this.closeModal(); + }, + closeModal() { + this.$nextTick(() => { + this.$refs.modal.hide(); + }); + }, + resetForm() { + this.form.defaultGateway = this.defaultGateway; + this.$v.$reset(); + this.$emit('hidden'); + }, + onOk(bvModalEvt) { + // prevent modal close + bvModalEvt.preventDefault(); + this.handleSubmit(); + }, + }, +}; +</script> diff --git a/src/views/Settings/Network/ModalIpv6.vue b/src/views/Settings/Network/ModalIpv6.vue new file mode 100644 index 00000000..f707a774 --- /dev/null +++ b/src/views/Settings/Network/ModalIpv6.vue @@ -0,0 +1,133 @@ +<template> + <b-modal + id="modal-add-ipv6" + ref="modal" + :title="$t('pageNetwork.table.addIpv6Address')" + @hidden="resetForm" + > + <b-form id="form-ipv6" @submit.prevent="handleSubmit"> + <b-row> + <b-col sm="6"> + <b-form-group + :label="$t('pageNetwork.modal.ipAddress')" + label-for="ipAddress" + > + <b-form-input + id="ipAddress" + v-model="form.ipAddress" + type="text" + :state="getValidationState($v.form.ipAddress)" + @input="$v.form.ipAddress.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <template v-if="!$v.form.ipAddress.required"> + {{ $t('global.form.fieldRequired') }} + </template> + <template v-if="!$v.form.ipAddress.validateIpv6"> + {{ $t('global.form.invalidFormat') }} + </template> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col sm="6"> + <b-form-group + :label="$t('pageNetwork.modal.prefixLength')" + label-for="prefixLength" + > + <b-form-input + id="prefixLength" + v-model="form.prefixLength" + type="text" + :state="getValidationState($v.form.prefixLength)" + @input="$v.form.prefixLength.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <template v-if="!$v.form.prefixLength.required"> + {{ $t('global.form.fieldRequired') }} + </template> + <template v-if="!$v.form.prefixLength.validatePrefixLength"> + {{ $t('global.form.invalidFormat') }} + </template> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + </b-row> + </b-form> + <template #modal-footer="{ cancel }"> + <b-button variant="secondary" @click="cancel()"> + {{ $t('global.action.cancel') }} + </b-button> + <b-button form="form-ipv6" type="submit" variant="primary" @click="onOk"> + {{ $t('global.action.add') }} + </b-button> + </template> + </b-modal> +</template> + +<script> +import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; +import { required, helpers } from 'vuelidate/lib/validators'; + +const validateIpv6 = helpers.regex( + 'validateIpv6', + /^((?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}|:(?::[a-fA-F0-9]{1,4}){1,7}|::|(?:[a-fA-F0-9]{1,4}:){6}(?:[0-9]{1,3}\.){3}[0-9]{1,3}|::(?:[a-fA-F0-9]{1,4}:){0,5}(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,5}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,4}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-fA-F0-9]{1,4}:){1,2}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|[a-fA-F0-9]{1,4}:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|::(?:[0-9]{1,3}\.){3}[0-9]{1,3})$/, +); + +const validatePrefixLength = helpers.regex( + 'validatePrefixLength', + /^(12[0-8]|1[0-9]|[1-9][0-9]|[0-9])$/, +); + +export default { + mixins: [VuelidateMixin], + data() { + return { + form: { + ipAddress: '', + prefixLength: '', + }, + }; + }, + validations() { + return { + form: { + ipAddress: { + required, + validateIpv6, + }, + prefixLength: { + required, + validatePrefixLength, + }, + }, + }; + }, + methods: { + handleSubmit() { + this.$v.$touch(); + if (this.$v.$invalid) return; + this.$emit('ok', { + Address: this.form.ipAddress, + PrefixLength: parseInt(this.form.prefixLength), + }); + this.closeModal(); + }, + closeModal() { + this.$nextTick(() => { + this.$refs.modal.hide(); + }); + }, + resetForm() { + this.form.ipAddress = null; + this.form.prefixLength = null; + this.$v.$reset(); + this.$emit('hidden'); + }, + onOk(bvModalEvt) { + // prevent modal close + bvModalEvt.preventDefault(); + this.handleSubmit(); + }, + }, +}; +</script> diff --git a/src/views/Settings/Network/Network.vue b/src/views/Settings/Network/Network.vue index f731c25f..0279cbe6 100644 --- a/src/views/Settings/Network/Network.vue +++ b/src/views/Settings/Network/Network.vue @@ -23,6 +23,8 @@ <network-interface-settings :tab-index="tabIndex" /> <!-- IPV4 table --> <table-ipv-4 :tab-index="tabIndex" /> + <!-- IPV6 table --> + <table-ipv-6 :tab-index="tabIndex" /> <!-- Static DNS table --> <table-dns :tab-index="tabIndex" /> </b-tab> @@ -33,9 +35,14 @@ </page-section> <!-- Modals --> <modal-ipv4 :default-gateway="defaultGateway" @ok="saveIpv4Address" /> + <modal-ipv6 @ok="saveIpv6Address" /> <modal-dns @ok="saveDnsAddress" /> <modal-hostname :hostname="currentHostname" @ok="saveSettings" /> <modal-mac-address :mac-address="currentMacAddress" @ok="saveSettings" /> + <modal-default-gateway + :default-gateway="ipv6DefaultGateway" + @ok="saveSettings" + /> </b-container> </template> @@ -44,14 +51,17 @@ import BVToastMixin from '@/components/Mixins/BVToastMixin'; import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin'; import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin'; import ModalMacAddress from './ModalMacAddress.vue'; +import ModalDefaultGateway from './ModalDefaultGateway.vue'; import ModalHostname from './ModalHostname.vue'; import ModalIpv4 from './ModalIpv4.vue'; +import ModalIpv6 from './ModalIpv6.vue'; import ModalDns from './ModalDns.vue'; import NetworkGlobalSettings from './NetworkGlobalSettings.vue'; import NetworkInterfaceSettings from './NetworkInterfaceSettings.vue'; import PageSection from '@/components/Global/PageSection'; import PageTitle from '@/components/Global/PageTitle'; import TableIpv4 from './TableIpv4.vue'; +import TableIpv6 from './TableIpv6.vue'; import TableDns from './TableDns.vue'; import { mapState } from 'vuex'; @@ -60,7 +70,9 @@ export default { components: { ModalHostname, ModalMacAddress, + ModalDefaultGateway, ModalIpv4, + ModalIpv6, ModalDns, NetworkGlobalSettings, NetworkInterfaceSettings, @@ -68,6 +80,7 @@ export default { PageTitle, TableDns, TableIpv4, + TableIpv6, }, mixins: [BVToastMixin, DataFormatterMixin, LoadingBarMixin], beforeRouteLeave(to, from, next) { @@ -79,6 +92,7 @@ export default { currentHostname: '', currentMacAddress: '', defaultGateway: '', + ipv6DefaultGateway: '', loading, tabIndex: 0, }; @@ -105,6 +119,9 @@ export default { const networkTableIpv4 = new Promise((resolve) => { this.$root.$on('network-table-ipv4-complete', () => resolve()); }); + const networkTableIpv6 = new Promise((resolve) => { + this.$root.$on('network-table-ipv6-complete', () => resolve()); + }); // Combine all child component Promises to indicate // when page data load complete Promise.all([ @@ -113,6 +130,7 @@ export default { interfaceSettings, networkTableDns, networkTableIpv4, + networkTableIpv6, ]).finally(() => this.endLoader()); }, methods: { @@ -131,6 +149,10 @@ export default { this.$store.getters['network/globalNetworkSettings'][ this.tabIndex ].macAddress; + this.ipv6DefaultGateway = + this.$store.getters['network/globalNetworkSettings'][ + this.tabIndex + ].ipv6DefaultGateway; }, getTabIndex(selectedIndex) { this.tabIndex = selectedIndex; @@ -149,6 +171,14 @@ export default { .catch(({ message }) => this.errorToast(message)) .finally(() => this.endLoader()); }, + saveIpv6Address(modalFormData) { + this.startLoader(); + this.$store + .dispatch('network/saveIpv6Address', modalFormData) + .then((message) => this.successToast(message)) + .catch(({ message }) => this.errorToast(message)) + .finally(() => this.endLoader()); + }, saveDnsAddress(modalFormData) { this.startLoader(); this.$store diff --git a/src/views/Settings/Network/NetworkGlobalSettings.vue b/src/views/Settings/Network/NetworkGlobalSettings.vue index 30287673..0c062ea2 100644 --- a/src/views/Settings/Network/NetworkGlobalSettings.vue +++ b/src/views/Settings/Network/NetworkGlobalSettings.vue @@ -4,7 +4,7 @@ :section-title="$t('pageNetwork.networkSettings')" > <b-row> - <b-col md="3"> + <b-col md="2"> <dl> <dt> {{ $t('pageNetwork.hostname') }} @@ -12,10 +12,19 @@ <icon-edit :title="$t('pageNetwork.modal.editHostnameTitle')" /> </b-button> </dt> - <dd>{{ dataFormatter(firstInterface.hostname) }}</dd> + <dd style="word-break: break-all"> + {{ dataFormatter(firstInterface.hostname) }} + </dd> </dl> </b-col> - <b-col md="3"> + <b-col md="2"> + <dl> + <dt>{{ $t('pageNetwork.ipVersion') }}</dt> + <dd>{{ $t('pageNetwork.ipv4') }}</dd> + <dd>{{ $t('pageNetwork.ipv6') }}</dd> + </dl> + </b-col> + <b-col md="2"> <dl> <dt>{{ $t('pageNetwork.useDomainName') }}</dt> <dd> @@ -32,9 +41,23 @@ <span v-else>{{ $t('global.status.disabled') }}</span> </b-form-checkbox> </dd> + <dd> + <b-form-checkbox + id="useDomainNameSwitchIpv6" + v-model="useDomainNameStateIpv6" + data-test-id="networkSettings-switch-useDomainNameIpv6" + switch + @change="changeDomainNameStateIpv6" + > + <span v-if="useDomainNameStateIpv6"> + {{ $t('global.status.enabled') }} + </span> + <span v-else>{{ $t('global.status.disabled') }}</span> + </b-form-checkbox> + </dd> </dl> </b-col> - <b-col md="3"> + <b-col md="2"> <dl> <dt>{{ $t('pageNetwork.useDns') }}</dt> <dd> @@ -51,9 +74,23 @@ <span v-else>{{ $t('global.status.disabled') }}</span> </b-form-checkbox> </dd> + <dd> + <b-form-checkbox + id="useDnsSwitchIpv6" + v-model="useDnsStateIpv6" + data-test-id="networkSettings-switch-useDnsIpv6" + switch + @change="changeDnsStateIpv6" + > + <span v-if="useDnsStateIpv6"> + {{ $t('global.status.enabled') }} + </span> + <span v-else>{{ $t('global.status.disabled') }}</span> + </b-form-checkbox> + </dd> </dl> </b-col> - <b-col md="3"> + <b-col md="2"> <dl> <dt>{{ $t('pageNetwork.useNtp') }}</dt> <dd> @@ -70,6 +107,20 @@ <span v-else>{{ $t('global.status.disabled') }}</span> </b-form-checkbox> </dd> + <dd> + <b-form-checkbox + id="useNtpSwitchIpv6" + v-model="useNtpStateIpv6" + data-test-id="networkSettings-switch-useNtpIpv6" + switch + @change="changeNtpStateIpv6" + > + <span v-if="useNtpStateIpv6"> + {{ $t('global.status.enabled') }} + </span> + <span v-else>{{ $t('global.status.disabled') }}</span> + </b-form-checkbox> + </dd> </dl> </b-col> </b-row> @@ -125,6 +176,33 @@ export default { return newValue; }, }, + useDomainNameStateIpv6: { + get() { + return this.$store.getters['network/globalNetworkSettings'][0] + .useDomainNameEnabledIpv6; + }, + set(newValue) { + return newValue; + }, + }, + useDnsStateIpv6: { + get() { + return this.$store.getters['network/globalNetworkSettings'][0] + .useDnsEnabledIpv6v6; + }, + set(newValue) { + return newValue; + }, + }, + useNtpStateIpv6: { + get() { + return this.$store.getters['network/globalNetworkSettings'][0] + .useNtpEnabledIpv6; + }, + set(newValue) { + return newValue; + }, + }, }, created() { this.$store.dispatch('network/getEthernetData').finally(() => { @@ -135,7 +213,10 @@ export default { methods: { changeDomainNameState(state) { this.$store - .dispatch('network/saveDomainNameState', state) + .dispatch('network/saveDomainNameState', { + domainState: state, + ipVersion: 'IPv4', + }) .then((success) => { this.successToast(success); }) @@ -143,14 +224,57 @@ export default { }, changeDnsState(state) { this.$store - .dispatch('network/saveDnsState', state) - .then((message) => this.successToast(message)) + .dispatch('network/saveDnsState', { + dnsState: state, + ipVersion: 'IPv4', + }) + .then((message) => { + this.successToast(message); + }) .catch(({ message }) => this.errorToast(message)); }, changeNtpState(state) { this.$store - .dispatch('network/saveNtpState', state) - .then((message) => this.successToast(message)) + .dispatch('network/saveNtpState', { + ntpState: state, + ipVersion: 'IPv4', + }) + .then((message) => { + this.successToast(message); + }) + .catch(({ message }) => this.errorToast(message)); + }, + changeDomainNameStateIpv6(state) { + this.$store + .dispatch('network/saveDomainNameState', { + domainState: state, + ipVersion: 'IPv6', + }) + .then((success) => { + this.successToast(success); + }) + .catch(({ message }) => this.errorToast(message)); + }, + changeDnsStateIpv6(state) { + this.$store + .dispatch('network/saveDnsState', { + dnsState: state, + ipVersion: 'IPv6', + }) + .then((message) => { + this.successToast(message); + }) + .catch(({ message }) => this.errorToast(message)); + }, + changeNtpStateIpv6(state) { + this.$store + .dispatch('network/saveNtpState', { + ntpState: state, + ipVersion: 'IPv6', + }) + .then((message) => { + this.successToast(message); + }) .catch(({ message }) => this.errorToast(message)); }, initSettingsModal() { diff --git a/src/views/Settings/Network/TableIpv6.vue b/src/views/Settings/Network/TableIpv6.vue new file mode 100644 index 00000000..5a16e9dc --- /dev/null +++ b/src/views/Settings/Network/TableIpv6.vue @@ -0,0 +1,289 @@ +<template> + <page-section :section-title="$t('pageNetwork.ipv6')"> + <b-row class="mb-4"> + <b-col lg="2" md="6"> + <dl> + <dt>{{ $t('pageNetwork.dhcp6') }}</dt> + <dd> + <b-form-checkbox + id="dhcp6Switch" + v-model="dhcp6EnabledState" + data-test-id="networkSettings-switch-dhcp6Enabled" + switch + @change="changeDhcp6EnabledState" + > + <span v-if="dhcp6EnabledState"> + {{ $t('global.status.enabled') }} + </span> + <span v-else>{{ $t('global.status.disabled') }}</span> + </b-form-checkbox> + </dd> + </dl> + </b-col> + <b-col lg="2" md="6"> + <dl class="text-nowrap"> + <dt> + {{ $t('pageNetwork.ipv6DefaultGateway') }} + <b-button + v-if="defaultGatewayEditable" + variant="link" + class="p-1" + @click="initDefaultGatewayModal()" + > + <icon-edit + :title="$t('pageNetwork.modal.editIPv6DefaultGatewayTitle')" + /> + </b-button> + </dt> + <dd> + {{ dataFormatter(defaultGateway) }} + </dd> + </dl> + </b-col> + </b-row> + <b-row> + <b-col> + <h3 class="h5"> + {{ $t('pageNetwork.ipv6Addresses') }} + </h3> + </b-col> + <b-col class="text-right"> + <b-button variant="primary" @click="initAddIpv6Address()"> + <icon-add /> + {{ $t('pageNetwork.table.addIpv6Address') }} + </b-button> + </b-col> + </b-row> + <b-table + responsive="md" + hover + :fields="ipv6TableFields" + :items="form.ipv6TableItems" + :empty-text="$t('global.table.emptyMessage')" + class="mb-0" + show-empty + > + <template #cell(actions)="{ item, index }"> + <table-row-action + v-for="(action, actionIndex) in filteredActions(item)" + :key="actionIndex" + :value="action.value" + :title="action.title" + :enabled="action.enabled" + @click-table-action="onIpv6TableAction(action, $event, index)" + > + <template #icon> + <icon-edit v-if="action.value === 'edit'" /> + <icon-trashcan v-if="action.value === 'delete'" /> + </template> + </table-row-action> + </template> + </b-table> + </page-section> +</template> + +<script> +import BVToastMixin from '@/components/Mixins/BVToastMixin'; +import IconAdd from '@carbon/icons-vue/es/add--alt/20'; +import IconEdit from '@carbon/icons-vue/es/edit/20'; +import IconTrashcan from '@carbon/icons-vue/es/trash-can/20'; +import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin'; +import PageSection from '@/components/Global/PageSection'; +import TableRowAction from '@/components/Global/TableRowAction'; +import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin'; +import { mapState } from 'vuex'; + +export default { + name: 'Ipv6Table', + components: { + IconAdd, + IconEdit, + IconTrashcan, + PageSection, + TableRowAction, + }, + mixins: [BVToastMixin, LoadingBarMixin, DataFormatterMixin], + props: { + tabIndex: { + type: Number, + default: 0, + }, + }, + data() { + return { + form: { + ipv6TableItems: [], + }, + actions: [ + { + value: 'edit', + title: this.$t('global.action.edit'), + }, + { + value: 'delete', + title: this.$t('global.action.delete'), + }, + ], + ipv6TableFields: [ + { + key: 'Address', + label: this.$t('pageNetwork.table.ipAddress'), + }, + { + key: 'PrefixLength', + label: this.$t('pageNetwork.table.prefixLength'), + }, + { + key: 'AddressOrigin', + label: this.$t('pageNetwork.table.addressOrigin'), + }, + { key: 'actions', label: '', tdClass: 'text-right' }, + ], + defaultGateway: '', + defaultGatewayEditable: + process.env.VUE_APP_ENV_NAME !== 'nvidia-bluefield', + }; + }, + computed: { + ...mapState('network', ['ethernetData']), + selectedInterface() { + return this.$store.getters['network/selectedInterfaceIndex']; + }, + dhcp6EnabledState: { + get() { + return ( + this.$store.getters['network/globalNetworkSettings'][ + this.selectedInterface + ].dhcp6Enabled === 'Enabled' + ); + }, + set(newValue) { + return newValue; + }, + }, + filteredActions() { + return (item) => { + if (item.AddressOrigin === 'DHCPv6' || item.AddressOrigin === 'SLAAC') { + return item.actions.filter((action) => action.value !== 'delete'); + } else { + return item.actions; + } + }; + }, + }, + watch: { + // Watch for change in tab index + tabIndex() { + this.getIpv6TableItems(); + this.getDefaultGateway(); + }, + ethernetData() { + this.getIpv6TableItems(); + this.getDefaultGateway(); + }, + }, + created() { + this.getIpv6TableItems(); + this.getDefaultGateway(); + this.$store.dispatch('network/getEthernetData').finally(() => { + // Emit initial data fetch complete to parent component + this.$root.$emit('network-table-ipv6-complete'); + }); + }, + methods: { + getDefaultGateway() { + this.defaultGateway = this.ethernetData[this.tabIndex].IPv6DefaultGateway; + }, + getIpv6TableItems() { + const index = this.tabIndex; + const addresses = + this.ethernetData[index].IPv6Addresses.filter( + (ipv6) => + ipv6.AddressOrigin === 'LinkLocal' || + ipv6.AddressOrigin === 'Static' || + ipv6.AddressOrigin === 'SLAAC' || + ipv6.AddressOrigin === 'DHCPv6', + ) || []; + this.form.ipv6TableItems = addresses.map((ipv6) => { + return { + Address: ipv6.Address, + PrefixLength: ipv6.PrefixLength, + AddressOrigin: ipv6.AddressOrigin, + actions: [ + { + value: 'delete', + title: this.$t('pageNetwork.table.deleteIpv6'), + }, + ], + }; + }); + }, + onIpv6TableAction(action, $event, index) { + if ($event === 'delete') { + this.deleteIpv6TableRow(index); + } + }, + deleteIpv6TableRow(index) { + const AddressOrigin = this.form.ipv6TableItems[index].AddressOrigin; + this.form.ipv6TableItems.splice(index, 1); + const newIpv6Array = this.form.ipv6TableItems.map((ipv6) => { + const { Address, PrefixLength } = ipv6; + return { + Address, + PrefixLength, + }; + }); + if ( + newIpv6Array.length == 0 && + (AddressOrigin === 'Static' || AddressOrigin === 'LinkLocal') + ) { + this.$store + .dispatch('network/saveDhcp6EnabledState', true) + .then((message) => this.successToast(message)) + .catch(({ message }) => this.errorToast(message)); + } + this.$store + .dispatch('network/editIpv6Address', newIpv6Array) + .then((message) => this.successToast(message)) + .catch(({ message }) => this.errorToast(message)); + }, + initAddIpv6Address() { + this.$bvModal.show('modal-add-ipv6'); + }, + changeDhcp6EnabledState(state) { + this.$bvModal + .msgBoxConfirm( + state + ? this.$t('pageNetwork.modal.confirmEnableDhcp') + : this.$t('pageNetwork.modal.confirmDisableDhcp'), + { + title: this.$t('pageNetwork.modal.dhcpConfirmTitle', { + dhcpState: state + ? this.$t('global.action.enable') + : this.$t('global.action.disable'), + }), + okTitle: state + ? this.$t('global.action.enable') + : this.$t('global.action.disable'), + okVariant: 'danger', + cancelTitle: this.$t('global.action.cancel'), + }, + ) + .then((dhcpEnableConfirmed) => { + if (dhcpEnableConfirmed) { + this.$store + .dispatch('network/saveDhcp6EnabledState', state) + .then((message) => this.successToast(message)) + .catch(({ message }) => this.errorToast(message)); + } else { + let onDhcpCancel = document.getElementById('dhcp6Switch'); + onDhcpCancel.checked = !state; + } + }); + }, + initDefaultGatewayModal() { + this.$bvModal.show('modal-default-gateway'); + }, + }, +}; +</script> |