Network settings: Edit hostname and MAC address

Adds modals to edit hostname and mac address per interface.

Signed-off-by: Dixsie Wolmers <dixsie@ibm.com>
Change-Id: I45d265c198afd1d9de9bb519a15a74e724f50f55
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index acf0119..132c750 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -640,6 +640,7 @@
     "ipv6": "IPv6",
     "linkStatus": "Link status",
     "macAddress": "MAC address",
+    "network": "network",
     "networkSettings": "Network settings",
     "ntp": "NTP server",
     "pageDescription": "Configure BMC network settings",
@@ -649,8 +650,10 @@
     "speed": "Speed (mbps)",
     "staticDns": "Static DNS",
     "modal": {
-      "ipAddress": "IP address",
+      "editHostnameTitle": "Edit hostname",
+      "editMacAddressTitle": "Edit MAC address",
       "gateway": "Gateway",
+      "ipAddress": "IP address",
       "staticDns": "Static DNS",
       "subnetMask": "Subnet mask"
     },
diff --git a/src/store/modules/Settings/NetworkStore.js b/src/store/modules/Settings/NetworkStore.js
index 176fcd7..54fb3e0 100644
--- a/src/store/modules/Settings/NetworkStore.js
+++ b/src/store/modules/Settings/NetworkStore.js
@@ -33,6 +33,7 @@
           IPv4Addresses,
           IPv4StaticAddresses,
           LinkStatus,
+          MACAddress,
         } = data;
         return {
           defaultGateway: IPv4StaticAddresses[0]?.Gateway, //First static gateway is the default gateway
@@ -40,6 +41,7 @@
             (ipv4) => ipv4.AddressOrigin === 'DHCP'
           ),
           hostname: HostName,
+          macAddress: MACAddress,
           linkStatus: LinkStatus,
           staticAddress: IPv4StaticAddresses[0]?.Address, // Display first static address on overview page
           useDnsEnabled: DHCPv4.UseDNSServers,
@@ -231,6 +233,27 @@
           );
         });
     },
+    async saveSettings({ state, dispatch }, interfaceSettingsForm) {
+      return api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`,
+          interfaceSettingsForm
+        )
+        .then(dispatch('getEthernetData'))
+        .then(() => {
+          return i18n.t('pageNetwork.toast.successSaveNetworkSettings', {
+            setting: i18n.t('pageNetwork.network'),
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageNetwork.toast.errorSaveNetworkSettings', {
+              setting: i18n.t('pageNetwork.network'),
+            })
+          );
+        });
+    },
     async saveDnsAddress({ dispatch, state }, dnsForm) {
       const newAddress = dnsForm;
       const originalAddresses =
diff --git a/src/views/Settings/Network/ModalHostname.vue b/src/views/Settings/Network/ModalHostname.vue
new file mode 100644
index 0000000..f3221ec
--- /dev/null
+++ b/src/views/Settings/Network/ModalHostname.vue
@@ -0,0 +1,110 @@
+<template>
+  <b-modal
+    id="modal-hostname"
+    ref="modal"
+    :title="$t('pageNetwork.modal.editHostnameTitle')"
+    @hidden="resetForm"
+  >
+    <b-form id="hostname-settings" @submit.prevent="handleSubmit">
+      <b-row>
+        <b-col sm="6">
+          <b-form-group
+            :label="$t('pageNetwork.hostname')"
+            label-for="hostname"
+          >
+            <b-form-input
+              id="hostname"
+              v-model="form.hostname"
+              type="text"
+              :state="getValidationState($v.form.hostname)"
+              @input="$v.form.hostname.$touch()"
+            />
+            <b-form-invalid-feedback role="alert">
+              <template v-if="!$v.form.hostname.required">
+                {{ $t('global.form.fieldRequired') }}
+              </template>
+              <template v-if="!$v.form.hostname.validateHostname">
+                {{ $t('global.form.lengthMustBeBetween', { min: 1, max: 64 }) }}
+              </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="hostname-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 validateHostname = helpers.regex('validateHostname', /^\S{0,64}$/);
+
+export default {
+  mixins: [VuelidateMixin],
+  props: {
+    hostname: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      form: {
+        hostname: '',
+      },
+    };
+  },
+  watch: {
+    hostname() {
+      this.form.hostname = this.hostname;
+    },
+  },
+  validations() {
+    return {
+      form: {
+        hostname: {
+          required,
+          validateHostname,
+        },
+      },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', { HostName: this.form.hostname });
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.hostname = this.hostname;
+      this.$v.$reset();
+      this.$emit('hidden');
+    },
+    onOk(bvModalEvt) {
+      // prevent modal close
+      bvModalEvt.preventDefault();
+      this.handleSubmit();
+    },
+  },
+};
+</script>
diff --git a/src/views/Settings/Network/ModalMacAddress.vue b/src/views/Settings/Network/ModalMacAddress.vue
new file mode 100644
index 0000000..d563f4c
--- /dev/null
+++ b/src/views/Settings/Network/ModalMacAddress.vue
@@ -0,0 +1,109 @@
+<template>
+  <b-modal
+    id="modal-mac-address"
+    ref="modal"
+    :title="$t('pageNetwork.modal.editMacAddressTitle')"
+    @hidden="resetForm"
+  >
+    <b-form id="mac-settings" @submit.prevent="handleSubmit">
+      <b-row>
+        <b-col sm="6">
+          <b-form-group
+            :label="$t('pageNetwork.macAddress')"
+            label-for="macAddress"
+          >
+            <b-form-input
+              id="mac-address"
+              v-model.trim="form.macAddress"
+              data-test-id="network-input-macAddress"
+              type="text"
+              :state="getValidationState($v.form.macAddress)"
+              @change="$v.form.macAddress.$touch()"
+            />
+            <b-form-invalid-feedback role="alert">
+              <div v-if="!$v.form.macAddress.required">
+                {{ $t('global.form.fieldRequired') }}
+              </div>
+              <div v-if="!$v.form.macAddress.macAddress">
+                {{ $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="mac-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 { macAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+  mixins: [VuelidateMixin],
+  props: {
+    macAddress: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      form: {
+        macAddress: '',
+      },
+    };
+  },
+  watch: {
+    macAddress() {
+      this.form.macAddress = this.macAddress;
+    },
+  },
+  validations() {
+    return {
+      form: {
+        macAddress: {
+          required,
+          macAddress: macAddress(),
+        },
+      },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', { MACAddress: this.form.macAddress });
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.macAddress = this.macAddress;
+      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 729a7a3..2abbcd7 100644
--- a/src/views/Settings/Network/Network.vue
+++ b/src/views/Settings/Network/Network.vue
@@ -4,7 +4,7 @@
     <!-- Global settings for all interfaces -->
     <network-global-settings />
     <!-- Interface tabs -->
-    <page-section v-if="ethernetData">
+    <page-section v-show="ethernetData">
       <b-row>
         <b-col>
           <b-card no-body>
@@ -34,6 +34,8 @@
     <!-- Modals -->
     <modal-ipv4 :default-gateway="defaultGateway" @ok="saveIpv4Address" />
     <modal-dns @ok="saveDnsAddress" />
+    <modal-hostname :hostname="currentHostname" @ok="saveSettings" />
+    <modal-mac-address :mac-address="currentMacAddress" @ok="saveSettings" />
   </b-container>
 </template>
 
@@ -41,6 +43,8 @@
 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 ModalHostname from './ModalHostname.vue';
 import ModalIpv4 from './ModalIpv4.vue';
 import ModalDns from './ModalDns.vue';
 import NetworkGlobalSettings from './NetworkGlobalSettings.vue';
@@ -54,6 +58,8 @@
 export default {
   name: 'Network',
   components: {
+    ModalHostname,
+    ModalMacAddress,
     ModalIpv4,
     ModalDns,
     NetworkGlobalSettings,
@@ -70,6 +76,8 @@
   },
   data() {
     return {
+      currentHostname: '',
+      currentMacAddress: '',
       defaultGateway: '',
       loading,
       tabIndex: 0,
@@ -80,7 +88,7 @@
   },
   watch: {
     ethernetData() {
-      this.getGateway();
+      this.getModalInfo();
     },
   },
   created() {
@@ -108,10 +116,18 @@
     ]).finally(() => this.endLoader());
   },
   methods: {
-    getGateway() {
+    getModalInfo() {
       this.defaultGateway = this.$store.getters[
         'network/globalNetworkSettings'
       ][this.tabIndex].defaultGateway;
+
+      this.currentHostname = this.$store.getters[
+        'network/globalNetworkSettings'
+      ][this.tabIndex].hostname;
+
+      this.currentMacAddress = this.$store.getters[
+        'network/globalNetworkSettings'
+      ][this.tabIndex].macAddress;
     },
     getTabIndex(selectedIndex) {
       this.tabIndex = selectedIndex;
@@ -120,9 +136,7 @@
         'network/setSelectedTabId',
         this.ethernetData[selectedIndex].Id
       );
-      this.defaultGateway = this.$store.getters[
-        'network/globalNetworkSettings'
-      ][this.tabIndex].defaultGateway;
+      this.getModalInfo();
     },
     saveIpv4Address(modalFormData) {
       this.startLoader();
@@ -140,6 +154,14 @@
         .catch(({ message }) => this.errorToast(message))
         .finally(() => this.endLoader());
     },
+    saveSettings(modalFormData) {
+      this.startLoader();
+      this.$store
+        .dispatch('network/saveSettings', modalFormData)
+        .then((message) => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => this.endLoader());
+    },
   },
 };
 </script>
diff --git a/src/views/Settings/Network/NetworkGlobalSettings.vue b/src/views/Settings/Network/NetworkGlobalSettings.vue
index fc82c86..3028767 100644
--- a/src/views/Settings/Network/NetworkGlobalSettings.vue
+++ b/src/views/Settings/Network/NetworkGlobalSettings.vue
@@ -6,7 +6,12 @@
     <b-row>
       <b-col md="3">
         <dl>
-          <dt>{{ $t('pageNetwork.hostname') }}</dt>
+          <dt>
+            {{ $t('pageNetwork.hostname') }}
+            <b-button variant="link" class="p-1" @click="initSettingsModal()">
+              <icon-edit :title="$t('pageNetwork.modal.editHostnameTitle')" />
+            </b-button>
+          </dt>
           <dd>{{ dataFormatter(firstInterface.hostname) }}</dd>
         </dl>
       </b-col>
@@ -73,13 +78,14 @@
 
 <script>
 import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconEdit from '@carbon/icons-vue/es/edit/16';
 import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
 import PageSection from '@/components/Global/PageSection';
 import { mapState } from 'vuex';
 
 export default {
   name: 'GlobalNetworkSettings',
-  components: { PageSection },
+  components: { IconEdit, PageSection },
   mixins: [BVToastMixin, DataFormatterMixin],
 
   data() {
@@ -147,6 +153,9 @@
         .then((message) => this.successToast(message))
         .catch(({ message }) => this.errorToast(message));
     },
+    initSettingsModal() {
+      this.$bvModal.show('modal-hostname');
+    },
   },
 };
 </script>
diff --git a/src/views/Settings/Network/NetworkInterfaceSettings.vue b/src/views/Settings/Network/NetworkInterfaceSettings.vue
index bdcba4d..023d29b 100644
--- a/src/views/Settings/Network/NetworkInterfaceSettings.vue
+++ b/src/views/Settings/Network/NetworkInterfaceSettings.vue
@@ -24,7 +24,9 @@
       <b-row>
         <b-col md="3">
           <dl>
-            <dt>{{ $t('pageNetwork.fqdn') }}</dt>
+            <dt>
+              {{ $t('pageNetwork.fqdn') }}
+            </dt>
             <dd>
               {{ dataFormatter(fqdn) }}
             </dd>
@@ -32,7 +34,18 @@
         </b-col>
         <b-col md="3">
           <dl class="text-nowrap">
-            <dt>{{ $t('pageNetwork.macAddress') }}</dt>
+            <dt>
+              {{ $t('pageNetwork.macAddress') }}
+              <b-button
+                variant="link"
+                class="p-1"
+                @click="initMacAddressModal()"
+              >
+                <icon-edit
+                  :title="$t('pageNetwork.modal.editMacAddressTitle')"
+                />
+              </b-button>
+            </dt>
             <dd>
               {{ dataFormatter(macAddress) }}
             </dd>
@@ -45,6 +58,7 @@
 
 <script>
 import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import IconEdit from '@carbon/icons-vue/es/edit/16';
 import PageSection from '@/components/Global/PageSection';
 import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
 import { mapState } from 'vuex';
@@ -52,6 +66,7 @@
 export default {
   name: 'Ipv4Table',
   components: {
+    IconEdit,
     PageSection,
   },
   mixins: [BVToastMixin, DataFormatterMixin],
@@ -94,6 +109,9 @@
       this.fqdn = this.ethernetData[this.selectedInterface].FQDN;
       this.macAddress = this.ethernetData[this.selectedInterface].MACAddress;
     },
+    initMacAddressModal() {
+      this.$bvModal.show('modal-mac-address');
+    },
   },
 };
 </script>