Network Settings: Add and Delete  IPV4 and DNS address

Adds ability to add or delete static ipv4 and dns
addesses per interface.

Signed-off-by: Dixsie Wolmers <dixsie@ibm.com>
Change-Id: Ie143ded2f173dd48f137471a684ba0d35ab0bf69
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 3bfc4a8..acf0119 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -651,9 +651,11 @@
     "modal": {
       "ipAddress": "IP address",
       "gateway": "Gateway",
+      "staticDns": "Static DNS",
       "subnetMask": "Subnet mask"
     },
     "table": {
+      "addDnsAddress": "Add IP address",
       "addIpv4Address": "Add static IPv4 address",
       "addressOrigin": "Address origin",
       "deleteDns": "Edit DNS address",
diff --git a/src/store/modules/Settings/NetworkStore.js b/src/store/modules/Settings/NetworkStore.js
index 5b95cb0..176fcd7 100644
--- a/src/store/modules/Settings/NetworkStore.js
+++ b/src/store/modules/Settings/NetworkStore.js
@@ -7,13 +7,20 @@
     ethernetData: [],
     firstInterfaceId: '', //used for setting global DHCP settings
     globalNetworkSettings: [],
+    selectedInterfaceId: '', // which tab is selected
+    selectedInterfaceIndex: 0, // which tab is selected
   },
   getters: {
     ethernetData: (state) => state.ethernetData,
     firstInterfaceId: (state) => state.firstInterfaceId,
     globalNetworkSettings: (state) => state.globalNetworkSettings,
+    selectedInterfaceId: (state) => state.selectedInterfaceId,
+    selectedInterfaceIndex: (state) => state.selectedInterfaceIndex,
   },
   mutations: {
+    setDomainNameState: (state, domainState) =>
+      (state.domainState = domainState),
+    setDnsState: (state, dnsState) => (state.dnsState = dnsState),
     setEthernetData: (state, ethernetData) =>
       (state.ethernetData = ethernetData),
     setFirstInterfaceId: (state, firstInterfaceId) =>
@@ -28,6 +35,7 @@
           LinkStatus,
         } = data;
         return {
+          defaultGateway: IPv4StaticAddresses[0]?.Gateway, //First static gateway is the default gateway
           dhcpAddress: IPv4Addresses.filter(
             (ipv4) => ipv4.AddressOrigin === 'DHCP'
           ),
@@ -40,6 +48,11 @@
         };
       });
     },
+    setNtpState: (state, ntpState) => (state.ntpState = ntpState),
+    setSelectedInterfaceId: (state, selectedInterfaceId) =>
+      (state.selectedInterfaceId = selectedInterfaceId),
+    setSelectedInterfaceIndex: (state, selectedInterfaceIndex) =>
+      (state.selectedInterfaceIndex = selectedInterfaceIndex),
   },
   actions: {
     async getEthernetData({ commit }) {
@@ -65,6 +78,7 @@
 
           commit('setEthernetData', ethernetData);
           commit('setFirstInterfaceId', firstInterfaceId);
+          commit('setSelectedInterfaceId', firstInterfaceId);
           commit('setGlobalNetworkSettings', ethernetInterfaces);
         })
         .catch((error) => {
@@ -158,6 +172,111 @@
           );
         });
     },
+    async setSelectedTabIndex({ commit }, tabIndex) {
+      commit('setSelectedInterfaceIndex', tabIndex);
+    },
+    async setSelectedTabId({ commit }, tabId) {
+      commit('setSelectedInterfaceId', tabId);
+    },
+    async saveIpv4Address({ dispatch, state }, ipv4Form) {
+      const originalAddresses = state.ethernetData[
+        state.selectedInterfaceIndex
+      ].IPv4StaticAddresses.map((ipv4) => {
+        const { Address, SubnetMask, Gateway } = ipv4;
+        return {
+          Address,
+          SubnetMask,
+          Gateway,
+        };
+      });
+      const newAddress = [ipv4Form];
+      return api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`,
+          { IPv4StaticAddresses: originalAddresses.concat(newAddress) }
+        )
+        .then(dispatch('getEthernetData'))
+        .then(() => {
+          return i18n.t('pageNetwork.toast.successSaveNetworkSettings', {
+            setting: i18n.t('pageNetwork.ipv4'),
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageNetwork.toast.errorSaveNetworkSettings', {
+              setting: i18n.t('pageNetwork.ipv4'),
+            })
+          );
+        });
+    },
+    async editIpv4Address({ dispatch, state }, ipv4TableData) {
+      return api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`,
+          { IPv4StaticAddresses: ipv4TableData }
+        )
+        .then(dispatch('getEthernetData'))
+        .then(() => {
+          return i18n.t('pageNetwork.toast.successSaveNetworkSettings', {
+            setting: i18n.t('pageNetwork.ipv4'),
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageNetwork.toast.errorSaveNetworkSettings', {
+              setting: i18n.t('pageNetwork.ipv4'),
+            })
+          );
+        });
+    },
+    async saveDnsAddress({ dispatch, state }, dnsForm) {
+      const newAddress = dnsForm;
+      const originalAddresses =
+        state.ethernetData[state.selectedInterfaceIndex].StaticNameServers;
+      const newDnsArray = originalAddresses.concat(newAddress);
+      return api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`,
+          { StaticNameServers: newDnsArray }
+        )
+        .then(dispatch('getEthernetData'))
+        .then(() => {
+          return i18n.t('pageNetwork.toast.successSaveNetworkSettings', {
+            setting: i18n.t('pageNetwork.dns'),
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageNetwork.toast.errorSaveNetworkSettings', {
+              setting: i18n.t('pageNetwork.dns'),
+            })
+          );
+        });
+    },
+    async editDnsAddress({ dispatch, state }, dnsTableData) {
+      return api
+        .patch(
+          `/redfish/v1/Managers/bmc/EthernetInterfaces/${state.selectedInterfaceId}`,
+          { StaticNameServers: dnsTableData }
+        )
+        .then(dispatch('getEthernetData'))
+        .then(() => {
+          return i18n.t('pageNetwork.toast.successSaveNetworkSettings', {
+            setting: i18n.t('pageNetwork.dns'),
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageNetwork.toast.errorSaveNetworkSettings', {
+              setting: i18n.t('pageNetwork.dns'),
+            })
+          );
+        });
+    },
   },
 };
 
diff --git a/src/views/Settings/Network/ModalDns.vue b/src/views/Settings/Network/ModalDns.vue
new file mode 100644
index 0000000..7f12717
--- /dev/null
+++ b/src/views/Settings/Network/ModalDns.vue
@@ -0,0 +1,92 @@
+<template>
+  <b-modal
+    id="modal-dns"
+    ref="modal"
+    :title="$t('pageNetwork.table.addDnsAddress')"
+    @hidden="resetForm"
+  >
+    <b-form id="form-dns" @submit.prevent="handleSubmit">
+      <b-row>
+        <b-col sm="6">
+          <b-form-group
+            :label="$t('pageNetwork.modal.staticDns')"
+            label-for="staticDns"
+          >
+            <b-form-input
+              id="staticDns"
+              v-model="form.staticDns"
+              type="text"
+              :state="getValidationState($v.form.staticDns)"
+              @input="$v.form.staticDns.$touch()"
+            />
+            <b-form-invalid-feedback role="alert">
+              <template v-if="!$v.form.staticDns.required">
+                {{ $t('global.form.fieldRequired') }}
+              </template>
+              <template v-if="!$v.form.staticDns.ipAddress">
+                {{ $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-dns" 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 { ipAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+  mixins: [VuelidateMixin],
+  data() {
+    return {
+      form: {
+        staticDns: null,
+      },
+    };
+  },
+  validations() {
+    return {
+      form: {
+        staticDns: {
+          required,
+          ipAddress,
+        },
+      },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', [this.form.staticDns]);
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.staticDns = null;
+      this.$v.$reset();
+      this.$emit('hidden');
+    },
+    onOk(bvModalEvt) {
+      // prevent modal close
+      bvModalEvt.preventDefault();
+      this.handleSubmit();
+    },
+  },
+};
+</script>
diff --git a/src/views/Settings/Network/ModalIpv4.vue b/src/views/Settings/Network/ModalIpv4.vue
new file mode 100644
index 0000000..dcf4a57
--- /dev/null
+++ b/src/views/Settings/Network/ModalIpv4.vue
@@ -0,0 +1,165 @@
+<template>
+  <b-modal
+    id="modal-add-ipv4"
+    ref="modal"
+    :title="$t('pageNetwork.table.addIpv4Address')"
+    @hidden="resetForm"
+  >
+    <b-form id="form-ipv4" @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.ipAddress">
+                {{ $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.gateway')"
+            label-for="gateway"
+          >
+            <b-form-input
+              id="gateway"
+              v-model="form.gateway"
+              type="text"
+              :state="getValidationState($v.form.gateway)"
+              @input="$v.form.gateway.$touch()"
+            />
+            <b-form-invalid-feedback role="alert">
+              <template v-if="!$v.form.gateway.required">
+                {{ $t('global.form.fieldRequired') }}
+              </template>
+              <template v-if="!$v.form.gateway.ipAddress">
+                {{ $t('global.form.invalidFormat') }}
+              </template>
+            </b-form-invalid-feedback>
+          </b-form-group>
+        </b-col>
+      </b-row>
+      <b-row>
+        <b-col sm="6">
+          <b-form-group
+            :label="$t('pageNetwork.modal.subnetMask')"
+            label-for="subnetMask"
+          >
+            <b-form-input
+              id="subnetMask"
+              v-model="form.subnetMask"
+              type="text"
+              :state="getValidationState($v.form.subnetMask)"
+              @input="$v.form.subnetMask.$touch()"
+            />
+            <b-form-invalid-feedback role="alert">
+              <template v-if="!$v.form.subnetMask.required">
+                {{ $t('global.form.fieldRequired') }}
+              </template>
+              <template v-if="!$v.form.subnetMask.ipAddress">
+                {{ $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-ipv4" 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 { ipAddress, required } from 'vuelidate/lib/validators';
+
+export default {
+  mixins: [VuelidateMixin],
+  props: {
+    defaultGateway: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      form: {
+        ipAddress: '',
+        gateway: '',
+        subnetMask: '',
+      },
+    };
+  },
+  watch: {
+    defaultGateway() {
+      this.form.gateway = this.defaultGateway;
+    },
+  },
+  validations() {
+    return {
+      form: {
+        ipAddress: {
+          required,
+          ipAddress,
+        },
+        gateway: {
+          required,
+          ipAddress,
+        },
+        subnetMask: {
+          required,
+          ipAddress,
+        },
+      },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', {
+        Address: this.form.ipAddress,
+        Gateway: this.form.gateway,
+        SubnetMask: this.form.subnetMask,
+      });
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.ipAddress = null;
+      this.form.gateway = this.defaultGateway;
+      this.form.subnetMask = 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 918c8e9..729a7a3 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>
+    <page-section v-if="ethernetData">
       <b-row>
         <b-col>
           <b-card no-body>
@@ -31,6 +31,9 @@
         </b-col>
       </b-row>
     </page-section>
+    <!-- Modals -->
+    <modal-ipv4 :default-gateway="defaultGateway" @ok="saveIpv4Address" />
+    <modal-dns @ok="saveDnsAddress" />
   </b-container>
 </template>
 
@@ -38,6 +41,8 @@
 import BVToastMixin from '@/components/Mixins/BVToastMixin';
 import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin';
 import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
+import ModalIpv4 from './ModalIpv4.vue';
+import ModalDns from './ModalDns.vue';
 import NetworkGlobalSettings from './NetworkGlobalSettings.vue';
 import NetworkInterfaceSettings from './NetworkInterfaceSettings.vue';
 import PageSection from '@/components/Global/PageSection';
@@ -49,6 +54,8 @@
 export default {
   name: 'Network',
   components: {
+    ModalIpv4,
+    ModalDns,
     NetworkGlobalSettings,
     NetworkInterfaceSettings,
     PageSection,
@@ -63,6 +70,7 @@
   },
   data() {
     return {
+      defaultGateway: '',
       loading,
       tabIndex: 0,
     };
@@ -70,6 +78,11 @@
   computed: {
     ...mapState('network', ['ethernetData']),
   },
+  watch: {
+    ethernetData() {
+      this.getGateway();
+    },
+  },
   created() {
     this.startLoader();
     const globalSettings = new Promise((resolve) => {
@@ -95,8 +108,37 @@
     ]).finally(() => this.endLoader());
   },
   methods: {
+    getGateway() {
+      this.defaultGateway = this.$store.getters[
+        'network/globalNetworkSettings'
+      ][this.tabIndex].defaultGateway;
+    },
     getTabIndex(selectedIndex) {
       this.tabIndex = selectedIndex;
+      this.$store.dispatch('network/setSelectedTabIndex', this.tabIndex);
+      this.$store.dispatch(
+        'network/setSelectedTabId',
+        this.ethernetData[selectedIndex].Id
+      );
+      this.defaultGateway = this.$store.getters[
+        'network/globalNetworkSettings'
+      ][this.tabIndex].defaultGateway;
+    },
+    saveIpv4Address(modalFormData) {
+      this.startLoader();
+      this.$store
+        .dispatch('network/saveIpv4Address', modalFormData)
+        .then((message) => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => this.endLoader());
+    },
+    saveDnsAddress(modalFormData) {
+      this.startLoader();
+      this.$store
+        .dispatch('network/saveDnsAddress', modalFormData)
+        .then((message) => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => this.endLoader());
     },
   },
 };
diff --git a/src/views/Settings/Network/TableDns.vue b/src/views/Settings/Network/TableDns.vue
index 2578ba3..569109f 100644
--- a/src/views/Settings/Network/TableDns.vue
+++ b/src/views/Settings/Network/TableDns.vue
@@ -2,6 +2,12 @@
   <page-section :section-title="$t('pageNetwork.staticDns')">
     <b-row>
       <b-col lg="6">
+        <div class="text-right">
+          <b-button variant="primary" @click="initDnsModal()">
+            <icon-add />
+            {{ $t('pageNetwork.table.addDnsAddress') }}
+          </b-button>
+        </div>
         <b-table
           responsive="md"
           hover
@@ -11,7 +17,7 @@
           class="mb-0"
           show-empty
         >
-          <template #cell(actions)="{ item }">
+          <template #cell(actions)="{ item, index }">
             <table-row-action
               v-for="(action, actionIndex) in item.actions"
               :key="actionIndex"
@@ -34,6 +40,7 @@
 
 <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 PageSection from '@/components/Global/PageSection';
@@ -43,6 +50,7 @@
 export default {
   name: 'DNSTable',
   components: {
+    IconAdd,
     IconEdit,
     IconTrashcan,
     PageSection,
@@ -87,6 +95,9 @@
     tabIndex() {
       this.getStaticDnsItems();
     },
+    ethernetData() {
+      this.getStaticDnsItems();
+    },
   },
   created() {
     this.getStaticDnsItems();
@@ -104,10 +115,6 @@
           address: server,
           actions: [
             {
-              value: 'edit',
-              title: this.$t('pageNetwork.table.editDns'),
-            },
-            {
               value: 'delete',
               title: this.$t('pageNetwork.table.deleteDns'),
             },
@@ -115,12 +122,24 @@
         };
       });
     },
-    onDnsTableAction(action, row) {
-      if (action === 'delete') {
-        this.form.dnsStaticTableItems.splice(row, 1);
-        // TODO: delete row in store
+    onDnsTableAction(action, $event, index) {
+      if ($event === 'delete') {
+        this.deleteDnsTableRow(index);
       }
     },
+    deleteDnsTableRow(index) {
+      this.form.dnsStaticTableItems.splice(index, 1);
+      const newDnsArray = this.form.dnsStaticTableItems.map((dns) => {
+        return dns.address;
+      });
+      this.$store
+        .dispatch('network/editDnsAddress', newDnsArray)
+        .then((message) => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message));
+    },
+    initDnsModal() {
+      this.$bvModal.show('modal-dns');
+    },
   },
 };
 </script>
diff --git a/src/views/Settings/Network/TableIpv4.vue b/src/views/Settings/Network/TableIpv4.vue
index 5e4bb7b..7587003 100644
--- a/src/views/Settings/Network/TableIpv4.vue
+++ b/src/views/Settings/Network/TableIpv4.vue
@@ -6,6 +6,12 @@
           {{ $t('pageNetwork.ipv4Addresses') }}
         </h3>
       </b-col>
+      <b-col class="text-right">
+        <b-button variant="primary" @click="initAddIpv4Address()">
+          <icon-add />
+          {{ $t('pageNetwork.table.addIpv4Address') }}
+        </b-button>
+      </b-col>
     </b-row>
     <b-table
       responsive="md"
@@ -16,7 +22,7 @@
       class="mb-0"
       show-empty
     >
-      <template #cell(actions)="{ item }">
+      <template #cell(actions)="{ item, index }">
         <table-row-action
           v-for="(action, actionIndex) in item.actions"
           :key="actionIndex"
@@ -37,8 +43,10 @@
 
 <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 { mapState } from 'vuex';
@@ -46,12 +54,13 @@
 export default {
   name: 'Ipv4Table',
   components: {
+    IconAdd,
     IconEdit,
     IconTrashcan,
     PageSection,
     TableRowAction,
   },
-  mixins: [BVToastMixin],
+  mixins: [BVToastMixin, LoadingBarMixin],
   props: {
     tabIndex: {
       type: Number,
@@ -102,6 +111,9 @@
     tabIndex() {
       this.getIpv4TableItems();
     },
+    ethernetData() {
+      this.getIpv4TableItems();
+    },
   },
   created() {
     this.getIpv4TableItems();
@@ -122,25 +134,36 @@
           AddressOrigin: ipv4.AddressOrigin,
           actions: [
             {
-              value: 'edit',
-              title: this.$t('pageNetwork.table.editIpv4'),
-              enabled: false,
-            },
-            {
               value: 'delete',
               title: this.$t('pageNetwork.table.deleteIpv4'),
-              enabled: false,
             },
           ],
         };
       });
     },
-    onIpv4TableAction(action, row) {
-      if (action === 'delete') {
-        this.form.ipv4TableItems.splice(row, 1);
-        // TODO: delete row in store
+    onIpv4TableAction(action, $event, index) {
+      if ($event === 'delete') {
+        this.deleteIpv4TableRow(index);
       }
     },
+    deleteIpv4TableRow(index) {
+      this.form.ipv4TableItems.splice(index, 1);
+      const newIpv4Array = this.form.ipv4TableItems.map((ipv4) => {
+        const { Address, SubnetMask, Gateway } = ipv4;
+        return {
+          Address,
+          SubnetMask,
+          Gateway,
+        };
+      });
+      this.$store
+        .dispatch('network/editIpv4Address', newIpv4Array)
+        .then((message) => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message));
+    },
+    initAddIpv4Address() {
+      this.$bvModal.show('modal-add-ipv4');
+    },
   },
 };
 </script>