diff options
author | Sean Zhang <xiazhang@nvidia.com> | 2024-06-12 10:28:41 +0300 |
---|---|---|
committer | Sean Zhang <xiazhang@nvidia.com> | 2024-07-06 05:02:17 +0300 |
commit | db47b7e1f4b8174698b1894edabf57d022fac609 (patch) | |
tree | e27e9e923037859dee7e7e814c4bc46a3e7b4ed3 | |
parent | 8841b7d463a5272a87faaa14cb103f778a772770 (diff) | |
download | webui-vue-db47b7e1f4b8174698b1894edabf57d022fac609.tar.xz |
Add support for IPv6 network setting
Add IPv6 setting in network setting page.
- Add IPv6 domain name, DNS servers, NTP servers enable/disable
- Add DHCPv6 enable/disable
- Add IPv6 default gateway
- Add IPv6 addresses
- Add IPv6 static addresses
- Add IPv6 static addresses adding and deleting
Tested:
- IPv6 domain name, DNS servers, NTP servers enable/disable function
- DHCPv6 enable/disable function
- Verified the IPv6 default gateway
- IPv6 addresses adding and deleting
- Verified the IPv6 addresses in IPv6 table
Change-Id: I9eebf6ef5f7de748f79779d8168b8dcfcdda2495
Signed-off-by: Sean Zhang <xiazhang@nvidia.com>
-rw-r--r-- | src/locales/en-US.json | 11 | ||||
-rw-r--r-- | src/store/modules/Settings/NetworkStore.js | 172 | ||||
-rw-r--r-- | src/views/Settings/Network/ModalDefaultGateway.vue | 114 | ||||
-rw-r--r-- | src/views/Settings/Network/ModalIpv6.vue | 133 | ||||
-rw-r--r-- | src/views/Settings/Network/Network.vue | 30 | ||||
-rw-r--r-- | src/views/Settings/Network/NetworkGlobalSettings.vue | 140 | ||||
-rw-r--r-- | src/views/Settings/Network/TableIpv6.vue | 289 |
7 files changed, 859 insertions, 30 deletions
diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 8ba7ac94..44a63de3 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -703,16 +703,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 +733,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 +743,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/store/modules/Settings/NetworkStore.js b/src/store/modules/Settings/NetworkStore.js index 7f24e198..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) => @@ -114,13 +132,49 @@ 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 @@ -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,13 +199,23 @@ 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 @@ -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,13 +239,23 @@ 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 @@ -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'), @@ -239,6 +317,37 @@ 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( @@ -260,6 +369,27 @@ 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( 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..db834047 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') }} @@ -15,7 +15,14 @@ <dd>{{ 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 +39,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 +72,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 +105,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 +174,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 +211,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 +222,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> |