Add dumps page to IBM env

Adds a non-functional dumps page with majority of layout and
user flows in place. Page visible with IBM dotenv configurations.

Includes:
- Initial GET for BMC dumps
- Table search, sort, filter, batch and row action flows
- New dump form, validations, and toast notification

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: Id59ec4327744f4e10cc99e4e3c0b2db5b0476f9c
diff --git a/src/env/components/AppNavigation/ibm.js b/src/env/components/AppNavigation/ibm.js
index a9870d3..37231f9 100644
--- a/src/env/components/AppNavigation/ibm.js
+++ b/src/env/components/AppNavigation/ibm.js
@@ -29,6 +29,11 @@
           icon: 'iconHealth',
           children: [
             {
+              id: 'dumps',
+              label: this.$t('appNavigation.dumps'),
+              route: '/health/dumps',
+            },
+            {
               id: 'event-logs',
               label: this.$t('appNavigation.eventLogs'),
               route: '/health/event-logs',
diff --git a/src/env/components/Dumps/Dumps.vue b/src/env/components/Dumps/Dumps.vue
new file mode 100644
index 0000000..eba90b7
--- /dev/null
+++ b/src/env/components/Dumps/Dumps.vue
@@ -0,0 +1,289 @@
+<template>
+  <b-container fluid="xl">
+    <page-title />
+    <b-row>
+      <b-col sm="6" lg="5" xl="4">
+        <page-section :section-title="$t('pageDumps.newDump')">
+          <dumps-form />
+        </page-section>
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col xl="10">
+        <page-section :section-title="$t('pageDumps.dumpHistory')">
+          <b-row class="align-items-start">
+            <b-col sm="8" xl="6" class="d-sm-flex align-items-end">
+              <search
+                :placeholder="$t('pageDumps.table.searchDumps')"
+                @change-search="onChangeSearchInput"
+                @clear-search="onClearSearchInput"
+              />
+              <div class="ml-sm-4">
+                <table-cell-count
+                  :filtered-items-count="filteredItemCount"
+                  :total-number-of-cells="tableItems.length"
+                ></table-cell-count>
+              </div>
+            </b-col>
+            <b-col sm="8" md="7" xl="6">
+              <table-date-filter @change="onChangeDateTimeFilter" />
+            </b-col>
+          </b-row>
+          <table-toolbar
+            :selected-items-count="selectedRows.length"
+            :actions="batchActions"
+            @clear-selected="clearSelectedRows($refs.table)"
+            @batch-action="onTableBatchAction"
+          />
+          <b-table
+            ref="table"
+            show-empty
+            hover
+            sort-icon-left
+            no-sort-reset
+            selectable
+            no-select-on-click
+            responsive="md"
+            sort-by="dateTime"
+            :fields="fields"
+            :items="filteredTableItems"
+            :empty-text="$t('global.table.emptyMessage')"
+            :empty-filtered-text="$t('global.table.emptySearchMessage')"
+            :filter="searchFilter"
+            @filtered="onChangeSearchFilter"
+            @row-selected="onRowSelected($event, filteredTableItems.length)"
+          >
+            <!-- Checkbox column -->
+            <template #head(checkbox)>
+              <b-form-checkbox
+                v-model="tableHeaderCheckboxModel"
+                :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"
+                @change="toggleSelectRow($refs.table, row.index)"
+              >
+                <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+              </b-form-checkbox>
+            </template>
+
+            <!-- Date and Time column -->
+            <template #cell(dateTime)="{ value }">
+              <p class="mb-0">{{ value | formatDate }}</p>
+              <p class="mb-0">{{ value | formatTime }}</p>
+            </template>
+
+            <!-- Size column -->
+            <template #cell(size)="{ value }">
+              {{ convertBytesToMegabytes(value) }} MB
+            </template>
+
+            <!-- Actions column -->
+            <template #cell(actions)="row">
+              <table-row-action
+                v-for="(action, index) in row.item.actions"
+                :key="index"
+                :value="action.value"
+                :title="action.title"
+                @click-table-action="onTableRowAction($event, row.item)"
+              >
+                <template #icon>
+                  <icon-delete v-if="action.value === 'delete'" />
+                </template>
+              </table-row-action>
+            </template>
+          </b-table>
+        </page-section>
+      </b-col>
+    </b-row>
+  </b-container>
+</template>
+
+<script>
+import IconDelete from '@carbon/icons-vue/es/trash-can/20';
+
+import DumpsForm from './DumpsForm';
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+import Search from '@/components/Global/Search';
+import TableCellCount from '@/components/Global/TableCellCount';
+import TableDateFilter from '@/components/Global/TableDateFilter';
+import TableRowAction from '@/components/Global/TableRowAction';
+import TableToolbar from '@/components/Global/TableToolbar';
+
+import BVTableSelectableMixin, {
+  selectedRows,
+  tableHeaderCheckboxModel,
+  tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import SearchFilterMixin, {
+  searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+
+export default {
+  components: {
+    DumpsForm,
+    IconDelete,
+    PageSection,
+    PageTitle,
+    Search,
+    TableCellCount,
+    TableDateFilter,
+    TableRowAction,
+    TableToolbar,
+  },
+  mixins: [
+    BVTableSelectableMixin,
+    LoadingBarMixin,
+    SearchFilterMixin,
+    TableFilterMixin,
+  ],
+  beforeRouteLeave(to, from, next) {
+    // Hide loader if the user navigates to another page
+    // before request is fulfilled.
+    this.hideLoader();
+    next();
+  },
+  data() {
+    return {
+      fields: [
+        {
+          key: 'checkbox',
+          sortable: false,
+        },
+        {
+          key: 'dateTime',
+          label: this.$t('pageDumps.table.dateAndTime'),
+          sortable: true,
+        },
+        {
+          key: 'dumpType',
+          label: this.$t('pageDumps.table.dumpType'),
+          sortable: true,
+        },
+        {
+          key: 'id',
+          label: this.$t('pageDumps.table.id'),
+          sortable: true,
+        },
+        {
+          key: 'size',
+          label: this.$t('pageDumps.table.size'),
+          sortable: true,
+        },
+        {
+          key: 'actions',
+          sortable: false,
+          label: '',
+          tdClass: 'text-right text-nowrap',
+        },
+      ],
+      batchActions: [
+        {
+          value: 'delete',
+          label: this.$t('global.action.delete'),
+        },
+      ],
+      filterEndDate: null,
+      filterStartDate: null,
+      searchFilter,
+      searchFilteredItemsCount: 0,
+      selectedRows,
+      tableHeaderCheckboxIndeterminate,
+      tableHeaderCheckboxModel,
+    };
+  },
+  computed: {
+    dumps() {
+      return this.$store.getters['dumps/allDumps'];
+    },
+    tableItems() {
+      return this.dumps.map((item) => {
+        return {
+          ...item,
+          actions: [
+            {
+              value: 'delete',
+              title: this.$t('global.action.delete'),
+            },
+          ],
+        };
+      });
+    },
+    filteredTableItems() {
+      return this.getFilteredTableDataByDate(
+        this.tableItems,
+        this.filterStartDate,
+        this.filterEndDate,
+        'dateTime'
+      );
+    },
+    filteredItemCount() {
+      return this.searchFilter
+        ? this.searchFilteredItemsCount
+        : this.filteredTableItems.length;
+    },
+  },
+  created() {
+    this.startLoader();
+    this.$store.dispatch('dumps/getBmcDumps').finally(() => this.endLoader());
+  },
+  methods: {
+    convertBytesToMegabytes(bytes) {
+      return parseFloat((bytes / 1000000).toFixed(3));
+    },
+    onChangeSearchFilter(items) {
+      this.searchFilteredItemsCount = items.length;
+    },
+    onChangeDateTimeFilter({ fromDate, toDate }) {
+      this.filterStartDate = fromDate;
+      this.filterEndDate = toDate;
+    },
+    onTableRowAction(action) {
+      if (action === 'delete') {
+        this.$bvModal
+          .msgBoxConfirm(this.$tc('pageDumps.modal.deleteDumpConfirmation'), {
+            title: this.$tc('pageDumps.modal.deleteDump'),
+            okTitle: this.$tc('pageDumps.modal.deleteDump'),
+            cancelTitle: this.$t('global.action.cancel'),
+          })
+          .then((deleteConfrimed) => {
+            if (deleteConfrimed); // delete dump
+          });
+      }
+    },
+    onTableBatchAction(action) {
+      if (action === 'delete') {
+        this.$bvModal
+          .msgBoxConfirm(
+            this.$tc(
+              'pageDumps.modal.deleteDumpConfirmation',
+              this.selectedRows.length
+            ),
+            {
+              title: this.$tc(
+                'pageDumps.modal.deleteDump',
+                this.selectedRows.length
+              ),
+              okTitle: this.$tc(
+                'pageDumps.modal.deleteDump',
+                this.selectedRows.length
+              ),
+              cancelTitle: this.$t('global.action.cancel'),
+            }
+          )
+          .then((deleteConfrimed) => {
+            if (deleteConfrimed); // delete dump
+          });
+      }
+    },
+  },
+};
+</script>
diff --git a/src/env/components/Dumps/DumpsForm.vue b/src/env/components/Dumps/DumpsForm.vue
new file mode 100644
index 0000000..ed81b3a
--- /dev/null
+++ b/src/env/components/Dumps/DumpsForm.vue
@@ -0,0 +1,61 @@
+<template>
+  <div class="form-background p-3">
+    <b-form id="form-new-dump" novalidate @submit.prevent="handleSubmit">
+      <b-form-group
+        :label="$t('pageDumps.form.selectDumpType')"
+        label-for="selectDumpType"
+      >
+        <b-form-select
+          id="selectDumpType"
+          v-model="selectedDumpType"
+          :options="dumpTypeOptions"
+          :state="getValidationState($v.selectedDumpType)"
+        >
+          <template #first>
+            <b-form-select-option :value="null" disabled>
+              {{ $t('global.form.selectAnOption') }}
+            </b-form-select-option>
+          </template>
+        </b-form-select>
+        <b-form-invalid-feedback role="alert">
+          {{ $t('global.form.required') }}
+        </b-form-invalid-feedback>
+      </b-form-group>
+      <b-button variant="primary" type="submit" form="form-new-dump">
+        {{ $t('pageDumps.form.createNewDump') }}
+      </b-button>
+    </b-form>
+  </div>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+export default {
+  mixins: [BVToastMixin, VuelidateMixin],
+  data() {
+    return {
+      selectedDumpType: null,
+      dumpTypeOptions: [
+        { value: 'bmc', text: this.$t('pageDumps.form.bmcDump') },
+        { value: 'system', text: this.$t('pageDumps.form.systemDump') },
+      ],
+    };
+  },
+  validations() {
+    return {
+      selectedDumpType: { required },
+    };
+  },
+  methods: {
+    handleSubmit() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.successToast(this.$t('pageDumps.toast.successStartDump'));
+    },
+  },
+};
+</script>
diff --git a/src/env/components/Dumps/index.js b/src/env/components/Dumps/index.js
new file mode 100644
index 0000000..65525fb
--- /dev/null
+++ b/src/env/components/Dumps/index.js
@@ -0,0 +1,2 @@
+import Dumps from './Dumps.vue';
+export default Dumps;
diff --git a/src/env/router/ibm.js b/src/env/router/ibm.js
index a647372..d4a6de0 100644
--- a/src/env/router/ibm.js
+++ b/src/env/router/ibm.js
@@ -24,6 +24,7 @@
 
 // Custom components
 import FirmwareSingleImage from '../components/FirmwareSingleImage';
+import Dumps from '../components/Dumps';
 
 const routes = [
   {
@@ -90,6 +91,14 @@
         },
       },
       {
+        path: '/health/dumps',
+        name: 'dumps',
+        component: Dumps,
+        meta: {
+          title: i18n.t('appPageTitle.dumps'),
+        },
+      },
+      {
         path: '/health/event-logs',
         name: 'event-logs',
         component: EventLogs,
diff --git a/src/env/store/Dumps/DumpsStore.js b/src/env/store/Dumps/DumpsStore.js
new file mode 100644
index 0000000..45f446c
--- /dev/null
+++ b/src/env/store/Dumps/DumpsStore.js
@@ -0,0 +1,32 @@
+import api from '@/store/api';
+
+const DumpsStore = {
+  namespaced: true,
+  state: {
+    bmcDumps: [],
+  },
+  getters: {
+    allDumps: (state) => state.bmcDumps,
+  },
+  mutations: {
+    setBmcDumps: (state, dumps) => {
+      state.bmcDumps = dumps.map((dump) => ({
+        dateTime: new Date(dump.Created),
+        dumpType: dump.Name,
+        id: dump.Id,
+        size: dump.AdditionalDataSizeBytes,
+        data: dump.AdditionalDataURI,
+      }));
+    },
+  },
+  actions: {
+    async getBmcDumps({ commit }) {
+      return await api
+        .get('/redfish/v1/Managers/bmc/LogServices/Dump/Entries')
+        .then(({ data = {} }) => commit('setBmcDumps', data.Members || []))
+        .catch((error) => console.log(error));
+    },
+  },
+};
+
+export default DumpsStore;
diff --git a/src/env/store/ibm.js b/src/env/store/ibm.js
index 383d97c..e19dd48 100644
--- a/src/env/store/ibm.js
+++ b/src/env/store/ibm.js
@@ -1,9 +1,11 @@
 import store from '@/store';
 import FirmwareSingleImageStore from './FirmwareSingleImage/FirmwareSingleImageStore';
+import DumpsStore from './Dumps/DumpsStore';
 
 store.unregisterModule('virtualMedia');
 
 store.unregisterModule('firmware');
 store.registerModule('firmwareSingleImage', FirmwareSingleImageStore);
+store.registerModule('dumps', DumpsStore);
 
 export default store;
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index dcb52a6..1d564ca 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -99,6 +99,7 @@
     "configuration": "Configuration",
     "control": "Control",
     "dateTimeSettings": "@:appPageTitle.dateTimeSettings",
+    "dumps": "@:appPageTitle.dumps",
     "eventLogs": "@:appPageTitle.eventLogs",
     "firmware": "@:appPageTitle.firmware",
     "hardwareStatus": "@:appPageTitle.hardwareStatus",
@@ -124,6 +125,7 @@
     "changePassword": "Change password",
     "clientSessions": "Client sessions",
     "dateTimeSettings": "Date and time settings",
+    "dumps": "Dumps",
     "eventLogs": "Event logs",
     "firmware": "Firmware",
     "hardwareStatus": "Hardware status",
@@ -198,6 +200,31 @@
       "successSaveDateTimeSettings": "Successfully saved date and time settings."
     }
   },
+  "pageDumps": {
+    "dumpHistory": "Dump history",
+    "newDump": "New dump",
+    "form": {
+      "bmcDump": "BMC dump",
+      "createNewDump": "Create new dump",
+      "selectDumpType": "Select dump type",
+      "systemDump": "System dump (disruptive)"
+    },
+    "modal": {
+      "deleteDump": "Delete dump | Delete dumps",
+      "deleteDumpConfirmation": "Are you sure you want to delete %{count} dump? This action cannot be undone. | Are you sure you want to delete %{count} dumps? This action cannot be undone."
+    },
+    "table": {
+      "createdBy": "Created by",
+      "dateAndTime": "Date and time",
+      "dumpType": "Dump type",
+      "id": "ID",
+      "searchDumps": "Search dumps",
+      "size": "Size"
+    },
+    "toast": {
+      "successStartDump": "Successfully started new dump."
+    }
+  },
   "pageEventLogs": {
     "exportFilePrefix": "event_logs_",
     "modal": {