Add network settings page

- Adds ability to configure newtowrk settings by selected ethernet interface
- Default gateway is currently unavailable in redfish,
  to work around, grabbed gateway from first static ipv4 configuration
  and assigned to new static ipv4 configurations
- Adds ability to add, modify and delete static ipv4 configs
- Adds ability to add, modify and delete static dns
- Adds ability to edit gateway, hostname and mac address
- Form validations include regex for ip,  mac address, and hostname

- Language translations to be addressed in separate commit
- Enabling DHCP and configuring DHCP settings to be addressed in separate commit

Signed-off-by: Dixsie Wolmers <dixsie@ibm.com>
Change-Id: I122034ae0ef3a8c08e5599ee3eca66e8d0d59f67
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index b7a3e07..8103558 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -59,7 +59,7 @@
               <b-nav-item href="javascript:void(0)">
                 {{ $t('appNavigation.firmware') }}
               </b-nav-item>
-              <b-nav-item href="javascript:void(0)">
+              <b-nav-item to="/configuration/network-settings">
                 {{ $t('appNavigation.networkSettings') }}
               </b-nav-item>
               <b-nav-item href="javascript:void(0)">
diff --git a/src/router/index.js b/src/router/index.js
index 30532a5..e35e0f5 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -72,6 +72,14 @@
         }
       },
       {
+        path: '/configuration/network-settings',
+        name: 'network-settings',
+        component: () => import('@/views/Configuration/NetworkSettings'),
+        meta: {
+          title: 'appPageTitle.networkSettings'
+        }
+      },
+      {
         path: '/control/reboot-bmc',
         name: 'reboot-bmc',
         component: () => import('@/views/Control/RebootBmc'),
diff --git a/src/store/modules/Configuration/NetworkSettingsStore.js b/src/store/modules/Configuration/NetworkSettingsStore.js
index f6912c8..524ad34 100644
--- a/src/store/modules/Configuration/NetworkSettingsStore.js
+++ b/src/store/modules/Configuration/NetworkSettingsStore.js
@@ -1,16 +1,25 @@
 import api from '../../api';
+import { find, remove } from 'lodash';
 
 const NetworkSettingsStore = {
   namespaced: true,
   state: {
-    ethernetData: []
+    defaultGateway: '',
+    ethernetData: [],
+    interfaceOptions: []
   },
   getters: {
-    ethernetData: state => state.ethernetData
+    defaultGateway: state => state.defaultGateway,
+    ethernetData: state => state.ethernetData,
+    interfaceOptions: state => state.interfaceOptions
   },
   mutations: {
+    setDefaultGateway: (state, defaultGateway) =>
+      (state.defaultGateway = defaultGateway),
     setEthernetData: (state, ethernetData) =>
-      (state.ethernetData = ethernetData)
+      (state.ethernetData = ethernetData),
+    setInterfaceOptions: (state, interfaceOptions) =>
+      (state.interfaceOptions = interfaceOptions)
   },
   actions: {
     async getEthernetData({ commit }) {
@@ -32,11 +41,67 @@
           const ethernetData = ethernetInterfaces.map(
             ethernetInterface => ethernetInterface.data
           );
+          const interfaceOptions = ethernetInterfaces.map(
+            ethernetName => ethernetName.data.Id
+          );
+          const addresses = ethernetData[0].IPv4StaticAddresses;
+
+          // Default gateway manually set to first gateway saved on the first interface. Default gateway property is WIP on backend
+          const defaultGateway = addresses.map(ipv4 => {
+            return ipv4.Gateway;
+          });
+
+          commit('setDefaultGateway', defaultGateway[0]);
           commit('setEthernetData', ethernetData);
+          commit('setInterfaceOptions', interfaceOptions);
         })
         .catch(error => {
           console.log('Network Data:', error);
         });
+    },
+
+    async updateInterfaceSettings({ dispatch, state }, networkSettingsForm) {
+      const updatedAddresses = networkSettingsForm.staticIpv4;
+      const originalAddresses =
+        state.ethernetData[networkSettingsForm.selectedInterfaceIndex]
+          .IPv4StaticAddresses;
+
+      const addressArray = originalAddresses.map(item => {
+        const address = item.Address;
+        if (find(updatedAddresses, { Address: address })) {
+          remove(updatedAddresses, item => {
+            return item.Address === address;
+          });
+          return {};
+        } else {
+          return null;
+        }
+      });
+
+      const data = {
+        HostName: networkSettingsForm.hostname,
+        MACAddress: networkSettingsForm.macAddress
+      };
+
+      // If DHCP disabled, update static DNS or static ipv4
+      if (!networkSettingsForm.isDhcpEnabled) {
+        data.IPv4StaticAddresses = [...addressArray, ...updatedAddresses];
+        data.StaticNameServers = networkSettingsForm.staticNameServers;
+      }
+
+      return await api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${networkSettingsForm.interfaceId}`,
+          data
+        )
+        .then(() => dispatch('getEthernetData'))
+        .then(() => {
+          return 'Successfully configured network settings.';
+        })
+        .catch(error => {
+          console.log(error);
+          throw new Error('Error configuring network settings.');
+        });
     }
   }
 };
diff --git a/src/views/Configuration/NetworkSettings/NetworkSettings.vue b/src/views/Configuration/NetworkSettings/NetworkSettings.vue
new file mode 100644
index 0000000..18e73a0
--- /dev/null
+++ b/src/views/Configuration/NetworkSettings/NetworkSettings.vue
@@ -0,0 +1,492 @@
+<template>
+  <b-container fluid="xl">
+    <page-title
+      description="Configure network settings for the BMC and the Virtualization management interface"
+    />
+    <page-section section-title="Interface">
+      <b-row>
+        <b-col lg="3">
+          <b-form-group label-for="interface-select" label="Network interface">
+            <b-form-select
+              id="interface-select"
+              v-model="selectedInterfaceIndex"
+              :options="interfaceSelectOptions"
+              @change="selectInterface"
+            >
+            </b-form-select>
+          </b-form-group>
+        </b-col>
+      </b-row>
+    </page-section>
+    <b-form novalidate @submit.prevent="submitForm">
+      <page-section section-title="System">
+        <b-row>
+          <b-col lg="3">
+            <b-form-group label="Default gateway" label-for="default-gateway">
+              <b-form-input
+                id="default-gateway"
+                v-model.trim="form.gateway"
+                type="text"
+                :readonly="dhcpEnabled"
+                :state="getValidationState($v.form.gateway)"
+                @change="$v.form.gateway.$touch()"
+              />
+              <b-form-invalid-feedback role="alert">
+                <div v-if="!$v.form.gateway.required">Field required</div>
+                <div v-if="!$v.form.gateway.validateAddress">Invalid</div>
+              </b-form-invalid-feedback>
+            </b-form-group>
+          </b-col>
+          <b-col lg="3">
+            <b-form-group label="Hostname" label-for="hostname-field">
+              <b-form-input
+                id="hostname-field"
+                v-model.trim="form.hostname"
+                type="text"
+                :state="getValidationState($v.form.hostname)"
+                @change="$v.form.hostname.$touch()"
+              />
+              <b-form-invalid-feedback role="alert">
+                <div v-if="!$v.form.hostname.required">Field required</div>
+                <div v-if="!$v.form.hostname.validateHostname">
+                  Must be less than 64 characters
+                </div>
+              </b-form-invalid-feedback>
+            </b-form-group>
+          </b-col>
+          <b-col lg="3">
+            <b-form-group label="MAC address" label-for="mac-address">
+              <b-form-input
+                id="mac-address"
+                v-model.trim="form.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">Field required</div>
+                <div v-if="!$v.form.macAddress.validateMacAddress">Invalid</div>
+              </b-form-invalid-feedback>
+            </b-form-group>
+          </b-col>
+        </b-row>
+      </page-section>
+      <page-section section-title="Static IPv4">
+        <b-row>
+          <b-col lg="9" class="mb-3">
+            <b-table
+              :fields="ipv4StaticTableFields"
+              :items="form.ipv4StaticTableItems"
+              class="mb-0"
+            >
+              <template v-slot:cell(Address)="{ item, index }">
+                <b-form-input
+                  v-model.trim="item.Address"
+                  :aria-label="'Static IPV4 address ' + (index + 1)"
+                  :readonly="dhcpEnabled"
+                  :state="
+                    getValidationState(
+                      $v.form.ipv4StaticTableItems.$each.$iter[index].Address
+                    )
+                  "
+                  @change="
+                    $v.form.ipv4StaticTableItems.$each.$iter[
+                      index
+                    ].Address.$touch()
+                  "
+                />
+                <b-form-invalid-feedback role="alert">
+                  <div
+                    v-if="
+                      !$v.form.ipv4StaticTableItems.$each.$iter[index].Address
+                        .required
+                    "
+                  >
+                    Field required
+                  </div>
+                  <div
+                    v-if="
+                      !$v.form.ipv4StaticTableItems.$each.$iter[index].Address
+                        .validateAddress
+                    "
+                  >
+                    Invalid
+                  </div>
+                </b-form-invalid-feedback>
+              </template>
+              <template v-slot:cell(SubnetMask)="{ item, index }">
+                <b-form-input
+                  v-model.trim="item.SubnetMask"
+                  :aria-label="'Static IPV4 Subnet mask ' + (index + 1)"
+                  :readonly="dhcpEnabled"
+                  :state="
+                    getValidationState(
+                      $v.form.ipv4StaticTableItems.$each.$iter[index].SubnetMask
+                    )
+                  "
+                  @change="
+                    $v.form.ipv4StaticTableItems.$each.$iter[
+                      index
+                    ].SubnetMask.$touch()
+                  "
+                />
+                <b-form-invalid-feedback role="alert">
+                  <div
+                    v-if="
+                      !$v.form.ipv4StaticTableItems.$each.$iter[index]
+                        .SubnetMask.required
+                    "
+                  >
+                    Field required
+                  </div>
+                  <div
+                    v-if="
+                      !$v.form.ipv4StaticTableItems.$each.$iter[index]
+                        .SubnetMask.validateAddress
+                    "
+                  >
+                    Invalid
+                  </div>
+                </b-form-invalid-feedback>
+              </template>
+              <template v-slot:cell(actions)="{ item, index }">
+                <table-row-action
+                  v-for="(action, actionIndex) in item.actions"
+                  :key="actionIndex"
+                  :value="action.value"
+                  :title="action.title"
+                  @click:tableAction="onDeleteIpv4StaticTableRow($event, index)"
+                >
+                  <template v-slot:icon>
+                    <icon-trashcan v-if="action.value === 'delete'" />
+                  </template>
+                </table-row-action>
+              </template>
+            </b-table>
+            <b-button variant="link" @click="addIpv4StaticTableRow">
+              <icon-add /> Add static IP
+            </b-button>
+          </b-col>
+        </b-row>
+      </page-section>
+      <page-section section-title="Static DNS">
+        <b-row>
+          <b-col lg="4" class="mb-3">
+            <b-table
+              :fields="dnsTableFields"
+              :items="form.dnsStaticTableItems"
+              class="mb-0"
+            >
+              <template v-slot:cell(address)="{ item, index }">
+                <b-form-input
+                  v-model.trim="item.address"
+                  :aria-label="'Static DNS ' + (index + 1)"
+                  :readonly="dhcpEnabled"
+                  :state="
+                    getValidationState(
+                      $v.form.dnsStaticTableItems.$each.$iter[index].address
+                    )
+                  "
+                  @change="
+                    $v.form.dnsStaticTableItems.$each.$iter[
+                      index
+                    ].address.$touch()
+                  "
+                />
+                <b-form-invalid-feedback role="alert">
+                  <div
+                    v-if="
+                      !$v.form.dnsStaticTableItems.$each.$iter[index].address
+                        .required
+                    "
+                  >
+                    Field required
+                  </div>
+                  <div
+                    v-if="
+                      !$v.form.dnsStaticTableItems.$each.$iter[index].address
+                        .validateAddress
+                    "
+                  >
+                    Invalid
+                  </div>
+                </b-form-invalid-feedback>
+              </template>
+              <template v-slot:cell(actions)="{ item, index }">
+                <table-row-action
+                  v-for="(action, actionIndex) in item.actions"
+                  :key="actionIndex"
+                  :value="action.value"
+                  :title="action.title"
+                  @click:tableAction="onDeleteDnsTableRow($event, index)"
+                >
+                  <template v-slot:icon>
+                    <icon-trashcan v-if="action.value === 'delete'" />
+                  </template>
+                </table-row-action>
+              </template>
+            </b-table>
+            <b-button variant="link" @click="addDnsTableRow">
+              <icon-add /> Add DNS server
+            </b-button>
+          </b-col>
+        </b-row>
+      </page-section>
+      <b-button
+        variant="primary"
+        type="submit"
+        :disabled="!$v.form.$anyDirty || $v.form.$invalid"
+      >
+        Save settings
+      </b-button>
+    </b-form>
+  </b-container>
+</template>
+
+<script>
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+import TableRowAction from '@/components/Global/TableRowAction';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin';
+import { mapState } from 'vuex';
+import { required, helpers } from 'vuelidate/lib/validators';
+
+// IP address, gateway and subnet pattern
+const validateAddress = helpers.regex(
+  'validateAddress',
+  /^(?=.*[^.]$)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.?){4}$/
+);
+// Hostname pattern
+const validateHostname = helpers.regex('validateHostname', /^\S{0,64}$/);
+// MAC address pattern
+const validateMacAddress = helpers.regex(
+  'validateMacAddress',
+  /^(?:[0-9A-Fa-f]{2}([:-]?)[0-9A-Fa-f]{2})(?:(?:\1|\.)(?:[0-9A-Fa-f]{2}([:-]?)[0-9A-Fa-f]{2})){2}$/
+);
+
+export default {
+  name: 'NetworkSettings',
+  components: {
+    PageTitle,
+    PageSection,
+    TableRowAction,
+    IconTrashcan,
+    IconAdd
+  },
+  mixins: [BVToastMixin, VuelidateMixin, LoadingBarMixin],
+  data() {
+    return {
+      dhcpEnabled: null,
+      ipv4Configuration: '',
+      ipv4StaticTableFields: [
+        { key: 'Address', label: 'IP address' },
+        { key: 'SubnetMask', label: 'Subnet mask' },
+        { key: 'actions', label: '', tdClass: 'text-right' }
+      ],
+      dnsTableFields: [
+        { key: 'address', label: 'IP address' },
+        { key: 'actions', label: '', tdClass: 'text-right' }
+      ],
+      selectedInterfaceIndex: 0,
+      selectedInterface: {},
+      form: {
+        gateway: '',
+        hostname: '',
+        macAddress: '',
+        ipv4StaticTableItems: [],
+        dnsStaticTableItems: []
+      }
+    };
+  },
+  validations() {
+    return {
+      form: {
+        gateway: { required, validateAddress },
+        hostname: { required, validateHostname },
+        ipv4StaticTableItems: {
+          $each: {
+            Address: {
+              required,
+              validateAddress
+            },
+            SubnetMask: {
+              required,
+              validateAddress
+            }
+          }
+        },
+        macAddress: { required, validateMacAddress },
+        dnsStaticTableItems: {
+          $each: {
+            address: {
+              required,
+              validateAddress
+            }
+          }
+        }
+      }
+    };
+  },
+  computed: {
+    ...mapState('networkSettings', [
+      'ethernetData',
+      'interfaceOptions',
+      'defaultGateway'
+    ]),
+    interfaceSelectOptions() {
+      return this.interfaceOptions.map((option, index) => {
+        return {
+          text: option,
+          value: index
+        };
+      });
+    }
+  },
+  watch: {
+    ethernetData: function() {
+      this.selectInterface();
+    }
+  },
+  created() {
+    this.startLoader();
+    this.$store
+      .dispatch('networkSettings/getEthernetData')
+      .finally(() => this.endLoader());
+  },
+  beforeRouteLeave(to, from, next) {
+    this.hideLoader();
+    next();
+  },
+  methods: {
+    selectInterface() {
+      this.selectedInterface = this.ethernetData[this.selectedInterfaceIndex];
+      this.getIpv4StaticTableItems();
+      this.getDnsStaticTableItems();
+      this.getInterfaceSettings();
+    },
+    getInterfaceSettings() {
+      this.form.gateway = this.defaultGateway;
+      this.form.hostname = this.selectedInterface.HostName;
+      this.form.macAddress = this.selectedInterface.MACAddress;
+      this.dhcpEnabled = this.selectedInterface.DHCPv4.DHCPEnabled;
+    },
+    getDnsStaticTableItems() {
+      const dns = this.selectedInterface.StaticNameServers || [];
+      this.form.dnsStaticTableItems = dns.map(server => {
+        return {
+          address: server,
+          actions: [
+            {
+              value: 'delete',
+              enabled: this.dhcpEnabled,
+              title: 'delete static dns row'
+            }
+          ]
+        };
+      });
+    },
+    addDnsTableRow() {
+      this.$v.form.dnsStaticTableItems.$touch();
+      this.form.dnsStaticTableItems.push({
+        address: '',
+        actions: [
+          {
+            value: 'delete',
+            enabled: this.dhcpEnabled,
+            title: 'delete static dns row'
+          }
+        ]
+      });
+    },
+    deleteDnsTableRow(index) {
+      this.$v.form.dnsStaticTableItems.$touch();
+      this.form.dnsStaticTableItems.splice(index, 1);
+    },
+    onDeleteDnsTableRow(action, row) {
+      this.deleteDnsTableRow(row);
+    },
+    getIpv4StaticTableItems() {
+      const addresses = this.selectedInterface.IPv4StaticAddresses || [];
+      this.form.ipv4StaticTableItems = addresses.map(ipv4 => {
+        return {
+          Address: ipv4.Address,
+          SubnetMask: ipv4.SubnetMask,
+          actions: [
+            {
+              value: 'delete',
+              enabled: this.dhcpEnabled,
+              title: 'delete static ipv4 row'
+            }
+          ]
+        };
+      });
+    },
+    addIpv4StaticTableRow() {
+      this.$v.form.ipv4StaticTableItems.$touch();
+      this.form.ipv4StaticTableItems.push({
+        Address: '',
+        SubnetMask: '',
+        actions: [
+          {
+            value: 'delete',
+            enabled: this.dhcpEnabled,
+            title: 'delete static ipv4 row'
+          }
+        ]
+      });
+    },
+    deleteIpv4StaticTableRow(index) {
+      this.$v.form.ipv4StaticTableItems.$touch();
+      this.form.ipv4StaticTableItems.splice(index, 1);
+    },
+    onDeleteIpv4StaticTableRow(action, row) {
+      this.deleteIpv4StaticTableRow(row);
+    },
+    submitForm() {
+      this.startLoader();
+      let networkInterfaceSelected = this.selectedInterface;
+      let selectedInterfaceIndex = this.selectedInterfaceIndex;
+      let interfaceId = networkInterfaceSelected.Id;
+      let isDhcpEnabled = networkInterfaceSelected.DHCPv4.DHCPEnabled;
+      let macAddress = this.form.macAddress;
+      let hostname = this.form.hostname;
+      let networkSettingsForm = {
+        interfaceId,
+        hostname,
+        macAddress,
+        selectedInterfaceIndex,
+        isDhcpEnabled
+      };
+      networkSettingsForm.staticIpv4 = this.form.ipv4StaticTableItems.map(
+        updateIpv4 => {
+          delete updateIpv4.actions;
+          updateIpv4.Gateway = this.form.gateway;
+          return updateIpv4;
+        }
+      );
+      networkSettingsForm.staticNameServers = this.form.dnsStaticTableItems.map(
+        updateDns => {
+          return updateDns.address;
+        }
+      );
+      this.$store
+        .dispatch(
+          'networkSettings/updateInterfaceSettings',
+          networkSettingsForm
+        )
+        .then(success => {
+          this.successToast(success);
+        })
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => {
+          this.$v.form.$reset();
+          this.endLoader();
+        });
+    }
+  }
+};
+</script>
diff --git a/src/views/Configuration/NetworkSettings/index.js b/src/views/Configuration/NetworkSettings/index.js
new file mode 100644
index 0000000..1215e1c
--- /dev/null
+++ b/src/views/Configuration/NetworkSettings/index.js
@@ -0,0 +1,2 @@
+import NetworkSettings from './NetworkSettings.vue';
+export default NetworkSettings;