Add SNMP alerts page and test hooks

This page will be included in Configuration section of the primary
navigation. The user will be able to delete and add alert destination.

Change-Id: I396d19a54ea11724f2c5aec67e20ba9abff947d3
Signed-off-by: Konstantin Aladyshev <aladyshev22@gmail.com>
diff --git a/src/views/Settings/SnmpAlerts/ModalAddDestination.vue b/src/views/Settings/SnmpAlerts/ModalAddDestination.vue
new file mode 100644
index 0000000..9637652
--- /dev/null
+++ b/src/views/Settings/SnmpAlerts/ModalAddDestination.vue
@@ -0,0 +1,145 @@
+<template>
+  <b-modal id="add-destination" ref="modal" @ok="onOk" @hidden="resetForm">
+    <template #modal-title>
+      {{ $t('pageSnmpAlerts.modal.addSnmpDestinationTitle') }}
+    </template>
+    <b-form id="form-destination">
+      <b-container>
+        <b-row>
+          <b-col sm="6">
+            <!-- Add new SNMP alert destination type -->
+            <b-form-group
+              :label="$t('pageSnmpAlerts.modal.ipaddress')"
+              label-for="ip-address"
+            >
+              <b-form-input
+                id="ip-Address"
+                v-model="form.ipAddress"
+                :state="getValidationState($v.form.ipAddress)"
+                data-test-id="snmpAlerts-input-ipAddress"
+                type="text"
+                @blur="$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>
+            <b-form-group label-for="port">
+              <template #label>
+                {{ $t('pageSnmpAlerts.modal.port') }} -
+                <span class="form-text d-inline">
+                  {{ $t('global.form.optional') }}
+                </span>
+              </template>
+              <b-form-input
+                id="port"
+                v-model="form.port"
+                type="text"
+                :state="getValidationState($v.form.port)"
+                data-test-id="snmpAlerts-input-port"
+                @blur="$v.form.port.$touch()"
+              />
+              <b-form-invalid-feedback role="alert">
+                <template
+                  v-if="!$v.form.port.minLength || !$v.form.port.maxLength"
+                >
+                  {{
+                    $t('global.form.valueMustBeBetween', {
+                      min: 0,
+                      max: 65535,
+                    })
+                  }}
+                </template>
+              </b-form-invalid-feedback>
+            </b-form-group>
+          </b-col>
+        </b-row>
+      </b-container>
+    </b-form>
+    <template #modal-footer="{ cancel }">
+      <b-button variant="secondary" @click="cancel()">
+        {{ $t('global.action.cancel') }}
+      </b-button>
+      <b-button
+        form="form-user"
+        type="submit"
+        variant="primary"
+        data-test-id="snmpAlerts-button-ok"
+        @click="onOk"
+      >
+        {{ $t('pageSnmpAlerts.addDestination') }}
+      </b-button>
+    </template>
+  </b-modal>
+</template>
+
+<script>
+import {
+  required,
+  ipAddress,
+  minValue,
+  maxValue,
+} from 'vuelidate/lib/validators';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+  mixins: [VuelidateMixin],
+  data() {
+    return {
+      form: {
+        ipaddress: null,
+        port: null,
+      },
+    };
+  },
+  validations() {
+    return {
+      form: {
+        ipAddress: {
+          required,
+          ipAddress,
+        },
+        port: {
+          minValue: minValue(0),
+          maxValue: maxValue(65535),
+        },
+      },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$emit('ok', {
+        ipAddress: this.form.ipAddress,
+        port: this.form.port,
+      });
+      this.closeModal();
+    },
+    closeModal() {
+      this.$nextTick(() => {
+        this.$refs.modal.hide();
+      });
+    },
+    resetForm() {
+      this.form.ipAddress = '';
+      this.form.port = '';
+      this.$v.$reset();
+      this.$emit('hidden');
+    },
+    onOk(bvModalEvt) {
+      // prevent modal close
+      bvModalEvt.preventDefault();
+      this.handleSubmit();
+    },
+  },
+};
+</script>
diff --git a/src/views/Settings/SnmpAlerts/SnmpAlerts.vue b/src/views/Settings/SnmpAlerts/SnmpAlerts.vue
new file mode 100644
index 0000000..8a9b8e7
--- /dev/null
+++ b/src/views/Settings/SnmpAlerts/SnmpAlerts.vue
@@ -0,0 +1,274 @@
+<template>
+  <b-container fluid="xl">
+    <page-title :description="$t('pageSnmpAlerts.pageDescription')" />
+    <b-row>
+      <b-col xl="9" class="text-right">
+        <b-button variant="primary" @click="initModalAddDestination">
+          <icon-add />
+          {{ $t('pageSnmpAlerts.addDestination') }}
+        </b-button>
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col xl="9">
+        <table-toolbar
+          ref="toolbar"
+          :selected-items-count="selectedRows.length"
+          :actions="tableToolbarActions"
+          @clear-selected="clearSelectedRows($refs.table)"
+          @batch-action="onBatchAction"
+        />
+        <b-table
+          ref="table"
+          responsive="md"
+          selectable
+          show-empty
+          no-select-on-click
+          hover
+          :fields="fields"
+          :items="tableItems"
+          :empty-text="$t('global.table.emptyMessage')"
+          @row-selected="onRowSelected($event, tableItems.length)"
+        >
+          <!-- Checkbox column -->
+          <template #head(checkbox)>
+            <b-form-checkbox
+              v-model="tableHeaderCheckboxModel"
+              data-test-id="snmpAlerts-checkbox-selectAll"
+              :indeterminate="tableHeaderCheckboxIndeterminate"
+              @change="onChangeHeaderCheckbox($refs.table)"
+            >
+              <span class="sr-only">{{ $t('global.table.selectAll') }}</span>
+            </b-form-checkbox>
+          </template>
+          <template #cell(checkbox)="row">
+            <b-form-checkbox
+              v-model="row.rowSelected"
+              :data-test-id="`snmpAlerts-checkbox-selectRow-${row.index}`"
+              @change="toggleSelectRow($refs.table, row.index)"
+            >
+              <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+            </b-form-checkbox>
+          </template>
+
+          <!-- table actions column -->
+          <template #cell(actions)="{ item }">
+            <table-row-action
+              v-for="(action, index) in item.actions"
+              :key="index"
+              :value="action.value"
+              :enabled="action.enabled"
+              :title="action.title"
+              :data-test-id="`snmpAlerts-button-deleteRow-${item.index}`"
+              @click-table-action="onTableRowAction($event, item)"
+            >
+              <template #icon>
+                <icon-trashcan v-if="action.value === 'delete'" />
+              </template>
+            </table-row-action>
+          </template>
+        </b-table>
+      </b-col>
+    </b-row>
+    <!-- Modals -->
+    <modal-add-destination @ok="onModalOk" />
+  </b-container>
+</template>
+
+<script>
+import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
+import ModalAddDestination from './ModalAddDestination';
+import PageTitle from '@/components/Global/PageTitle';
+import IconAdd from '@carbon/icons-vue/es/add--alt/20';
+import TableToolbar from '@/components/Global/TableToolbar';
+import TableRowAction from '@/components/Global/TableRowAction';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+import BVTableSelectableMixin, {
+  selectedRows,
+  tableHeaderCheckboxModel,
+  tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+export default {
+  name: 'SnmpAlerts',
+  components: {
+    PageTitle,
+    IconAdd,
+    TableToolbar,
+    IconTrashcan,
+    ModalAddDestination,
+    TableRowAction,
+  },
+  mixins: [BVTableSelectableMixin, BVToastMixin, LoadingBarMixin],
+  beforeRouteLeave(to, from, next) {
+    this.hideLoader();
+    next();
+  },
+  data() {
+    return {
+      fields: [
+        {
+          key: 'checkbox',
+        },
+        {
+          key: 'IP',
+          label: this.$t('pageSnmpAlerts.table.ipaddress'),
+        },
+        {
+          key: 'Port',
+          label: this.$t('pageSnmpAlerts.table.port'),
+        },
+        {
+          key: 'actions',
+          label: '',
+          tdClass: 'text-right text-nowrap',
+        },
+      ],
+      tableToolbarActions: [
+        {
+          value: 'delete',
+          label: this.$t('global.action.delete'),
+        },
+      ],
+      selectedRows: selectedRows,
+      tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+      tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+    };
+  },
+  computed: {
+    allSnmpDetails() {
+      return this.$store.getters['snmpAlerts/allSnmpDetails'];
+    },
+    tableItems() {
+      // transform destination data to table data
+      return this.allSnmpDetails.map((subscriptions) => {
+        const [destination, dataWithProtocol, dataWithoutProtocol] = [
+          subscriptions.Destination,
+          subscriptions.Destination.split('/')[2].split(':'),
+          subscriptions.Destination.split(':'),
+        ];
+        //condition to check if destination comes with protocol or not
+        const conditionForProtocolCheck = destination.includes('://');
+        const ip = conditionForProtocolCheck
+          ? dataWithProtocol[0]
+          : dataWithoutProtocol[0];
+        const port = conditionForProtocolCheck
+          ? dataWithProtocol[1]
+          : dataWithoutProtocol[1];
+        return {
+          IP: ip,
+          Port: port,
+          id: subscriptions.Id,
+          actions: [
+            {
+              value: 'delete',
+              enabled: true,
+              title: this.$tc('pageSnmpAlerts.deleteDestination'),
+            },
+          ],
+          ...subscriptions,
+        };
+      });
+    },
+  },
+  created() {
+    this.startLoader();
+    this.$store
+      .dispatch('snmpAlerts/getSnmpDetails')
+      .finally(() => this.endLoader());
+  },
+  methods: {
+    onModalOk({ ipAddress, port }) {
+      const protocolIpAddress = 'snmp://' + ipAddress;
+      const destination = port
+        ? protocolIpAddress + ':' + port
+        : protocolIpAddress;
+      const data = {
+        Destination: destination,
+        SubscriptionType: 'SNMPTrap',
+        Protocol: 'SNMPv2c',
+      };
+      this.startLoader();
+      this.$store
+        .dispatch('snmpAlerts/addDestination', { data })
+        .then((success) => this.successToast(success))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => this.endLoader());
+    },
+    initModalAddDestination() {
+      this.$bvModal.show('add-destination');
+    },
+    initModalDeleteDestination(destination) {
+      this.$bvModal
+        .msgBoxConfirm(
+          this.$t('pageSnmpAlerts.modal.deleteConfirmMessage', {
+            destination: destination.id,
+          }),
+          {
+            title: this.$tc('pageSnmpAlerts.modal.deleteSnmpDestinationTitle'),
+            okTitle: this.$tc('pageSnmpAlerts.deleteDestination'),
+            cancelTitle: this.$t('global.action.cancel'),
+          }
+        )
+        .then((deleteConfirmed) => {
+          if (deleteConfirmed) {
+            this.deleteDestination(destination);
+          }
+        });
+    },
+    deleteDestination({ id }) {
+      this.startLoader();
+      this.$store
+        .dispatch('snmpAlerts/deleteDestination', id)
+        .then((success) => this.successToast(success))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => this.endLoader());
+    },
+    onBatchAction(action) {
+      if (action === 'delete') {
+        this.$bvModal
+          .msgBoxConfirm(
+            this.$tc(
+              'pageSnmpAlerts.modal.batchDeleteConfirmMessage',
+              this.selectedRows.length
+            ),
+            {
+              title: this.$tc(
+                'pageSnmpAlerts.modal.deleteSnmpDestinationTitle',
+                this.selectedRows.length
+              ),
+              okTitle: this.$tc(
+                'pageSnmpAlerts.deleteDestination',
+                this.selectedRows.length
+              ),
+              cancelTitle: this.$t('global.action.cancel'),
+            }
+          )
+          .then((deleteConfirmed) => {
+            if (deleteConfirmed) {
+              this.startLoader();
+              this.$store
+                .dispatch(
+                  'snmpAlerts/deleteMultipleDestinations',
+                  this.selectedRows
+                )
+                .then((messages) => {
+                  messages.forEach(({ type, message }) => {
+                    if (type === 'success') this.successToast(message);
+                    if (type === 'error') this.errorToast(message);
+                  });
+                })
+                .finally(() => this.endLoader());
+            }
+          });
+      }
+    },
+    onTableRowAction(action, row) {
+      if (action === 'delete') {
+        this.initModalDeleteDestination(row);
+      }
+    },
+  },
+};
+</script>
diff --git a/src/views/Settings/SnmpAlerts/index.js b/src/views/Settings/SnmpAlerts/index.js
new file mode 100644
index 0000000..f27ed4a
--- /dev/null
+++ b/src/views/Settings/SnmpAlerts/index.js
@@ -0,0 +1,2 @@
+import SnmpAlerts from './SnmpAlerts.vue';
+export default SnmpAlerts;