Add POST code logs page

This page will be included in the Health section of the primary
navigation. The user will be able to export and download POST code
logs.

Signed-off-by: Sandeepa Singh <sandeepa.singh@ibm.com>
Change-Id: I26cf1e01bfdfcf298f24f2c7dd9633ab7d31f1b5
diff --git a/src/components/AppNavigation/AppNavigationMixin.js b/src/components/AppNavigation/AppNavigationMixin.js
index 5885219..f9ba077 100644
--- a/src/components/AppNavigation/AppNavigationMixin.js
+++ b/src/components/AppNavigation/AppNavigationMixin.js
@@ -39,6 +39,11 @@
               route: '/health/hardware-status',
             },
             {
+              id: 'post-code-logs',
+              label: this.$t('appNavigation.postCodeLogs'),
+              route: '/health/post-code-logs',
+            },
+            {
               id: 'sensors',
               label: this.$t('appNavigation.sensors'),
               route: '/health/sensors',
diff --git a/src/components/Global/TableRowAction.vue b/src/components/Global/TableRowAction.vue
index 9d853bc..99fa58b 100644
--- a/src/components/Global/TableRowAction.vue
+++ b/src/components/Global/TableRowAction.vue
@@ -13,6 +13,18 @@
       <span v-if="btnIconOnly" class="sr-only">{{ title }}</span>
     </b-link>
     <b-link
+      v-else-if="value === 'download' && downloadInNewTab"
+      class="align-bottom btn-icon-only py-0 btn-link"
+      target="_blank"
+      :href="downloadLocation"
+      :title="title"
+    >
+      <slot name="icon" />
+      <span class="sr-only">
+        {{ $t('global.action.download') }}
+      </span>
+    </b-link>
+    <b-link
       v-else-if="value === 'download'"
       class="align-bottom btn-icon-only py-0 btn-link"
       :download="exportName"
@@ -74,6 +86,10 @@
       type: Boolean,
       default: true,
     },
+    downloadInNewTab: {
+      type: Boolean,
+      default: false,
+    },
   },
   computed: {
     dataForExport() {
diff --git a/src/env/components/AppNavigation/ibm.js b/src/env/components/AppNavigation/ibm.js
index d4b8e3d..b8186a4 100644
--- a/src/env/components/AppNavigation/ibm.js
+++ b/src/env/components/AppNavigation/ibm.js
@@ -44,6 +44,11 @@
               route: '/health/hardware-status',
             },
             {
+              id: 'post-code-logs',
+              label: this.$t('appNavigation.postCodeLogs'),
+              route: '/health/post-code-logs',
+            },
+            {
               id: 'sensors',
               label: this.$t('appNavigation.sensors'),
               route: '/health/sensors',
diff --git a/src/env/router/ibm.js b/src/env/router/ibm.js
index e0586e8..91b70a7 100644
--- a/src/env/router/ibm.js
+++ b/src/env/router/ibm.js
@@ -15,6 +15,7 @@
 import NetworkSettings from '@/views/Configuration/NetworkSettings';
 import Overview from '@/views/Overview';
 import PageNotFound from '@/views/PageNotFound';
+import PostCodeLogs from '@/views/Health/PostCodeLogs';
 import PowerRestorePolicy from '@/views/Control/PowerRestorePolicy';
 import ProfileSettings from '@/views/ProfileSettings';
 import RebootBmc from '@/views/Control/RebootBmc';
@@ -119,6 +120,14 @@
         },
       },
       {
+        path: '/health/post-code-logs',
+        name: 'post-code-logs',
+        component: PostCodeLogs,
+        meta: {
+          title: i18n.t('appPageTitle.postCodeLogs'),
+        },
+      },
+      {
         path: '/health/sensors',
         name: 'sensors',
         component: Sensors,
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index c9e6bc5..a98625b 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -114,6 +114,7 @@
     "networkSettings": "@:appPageTitle.networkSettings",
     "overview": "@:appPageTitle.overview",
     "primaryNavigation": "Primary navigation",
+    "postCodeLogs": "@:appPageTitle.postCodeLogs",
     "powerRestorePolicy": "@:appPageTitle.powerRestorePolicy",
     "rebootBmc": "@:appPageTitle.rebootBmc",
     "securitySettings": "@:appPageTitle.securitySettings",
@@ -142,6 +143,7 @@
     "networkSettings": "Network settings",
     "overview": "Overview",
     "pageNotFound": "Page not found",
+    "postCodeLogs": "POST code logs",
     "powerRestorePolicy": "Power restore policy",
     "profileSettings": "Profile settings",
     "rebootBmc": "Reboot BMC",
@@ -587,6 +589,25 @@
       "solConsole": "@:appNavigation.serialOverLan"
     }
   },
+  "pagePostCodeLogs":{
+    "allExportFilePrefix": "All_POST_codes_log_",
+    "downloadFilePrefix": "POST_codes_additional_details_",
+    "exportFilePrefix": "POST_codes_log_",
+    "action": {
+      "downloadDetails": "Download additional details",
+      "exportLogs": "Export log"
+    },
+    "button": {
+     "exportAll": "Export all"
+    },
+    "table": {
+      "created": "Created",
+      "bootCount": "Boot count",
+      "postCode": "POST code",
+      "searchLogs": "Search logs",
+      "timeStampOffset": "Time stamp offset"
+    }
+  },
   "pageProfileSettings": {
     "browserOffset": "Browser offset (%{timezone})",
     "changePassword": "Change password",
diff --git a/src/router/routes.js b/src/router/routes.js
index e5812e0..b01b0da 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -17,6 +17,7 @@
 import NetworkSettings from '@/views/Configuration/NetworkSettings';
 import Overview from '@/views/Overview';
 import PageNotFound from '@/views/PageNotFound';
+import PostCodeLogs from '@/views/Health/PostCodeLogs';
 import PowerRestorePolicy from '@/views/Control/PowerRestorePolicy';
 import ProfileSettings from '@/views/ProfileSettings';
 import RebootBmc from '@/views/Control/RebootBmc';
@@ -119,6 +120,14 @@
         },
       },
       {
+        path: '/health/post-codes-logs',
+        name: 'post-codes-logs',
+        component: PostCodeLogs,
+        meta: {
+          title: i18n.t('appPageTitle.postCodeLogs'),
+        },
+      },
+      {
         path: '/health/sensors',
         name: 'sensors',
         component: Sensors,
diff --git a/src/store/index.js b/src/store/index.js
index 82efab9..29dfe4f 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -23,6 +23,7 @@
 import ChassisStore from './modules/Health/ChassisStore';
 import BmcStore from './modules/Health/BmcStore';
 import ProcessorStore from './modules/Health/ProcessorStore';
+import PostCodeLogsStore from './modules/Health/PostCodeLogsStore';
 import SecuritySettingsStore from './modules/Configuration/SecuritySettingsStore';
 import FactoryResetStore from './modules/Control/FactoryResetStore';
 
@@ -60,6 +61,7 @@
     chassis: ChassisStore,
     bmc: BmcStore,
     processors: ProcessorStore,
+    postCodeLogs: PostCodeLogsStore,
     virtualMedia: VirtualMediaStore,
     securitySettings: SecuritySettingsStore,
     factoryReset: FactoryResetStore,
diff --git a/src/store/modules/Health/PostCodeLogsStore.js b/src/store/modules/Health/PostCodeLogsStore.js
new file mode 100644
index 0000000..ac470ec
--- /dev/null
+++ b/src/store/modules/Health/PostCodeLogsStore.js
@@ -0,0 +1,39 @@
+import api from '@/store/api';
+
+const PostCodeLogsStore = {
+  namespaced: true,
+  state: {
+    allPostCodes: [],
+  },
+  getters: {
+    allPostCodes: (state) => state.allPostCodes,
+  },
+  mutations: {
+    setAllPostCodes: (state, allPostCodes) =>
+      (state.allPostCodes = allPostCodes),
+  },
+  actions: {
+    async getPostCodesLogData({ commit }) {
+      return await api
+        .get('/redfish/v1/Systems/system/LogServices/PostCodes/Entries')
+        .then(({ data: { Members = [] } = {} }) => {
+          const postCodeLogs = Members.map((log) => {
+            const { Created, MessageArgs, AdditionalDataURI } = log;
+            return {
+              date: new Date(Created),
+              bootCount: MessageArgs[0],
+              timeStampOffset: MessageArgs[1],
+              postCode: MessageArgs[2],
+              uri: AdditionalDataURI,
+            };
+          });
+          commit('setAllPostCodes', postCodeLogs);
+        })
+        .catch((error) => {
+          console.log('POST Codes Log Data:', error);
+        });
+    },
+  },
+};
+
+export default PostCodeLogsStore;
diff --git a/src/views/Health/PostCodeLogs/PostCodeLogs.vue b/src/views/Health/PostCodeLogs/PostCodeLogs.vue
new file mode 100644
index 0000000..1154cbf
--- /dev/null
+++ b/src/views/Health/PostCodeLogs/PostCodeLogs.vue
@@ -0,0 +1,344 @@
+<template>
+  <b-container fluid="xl">
+    <page-title />
+    <b-row class="align-items-start">
+      <b-col sm="8" xl="6" class="d-sm-flex align-items-end mb-4">
+        <search
+          :placeholder="$t('pagePostCodeLogs.table.searchLogs')"
+          @change-search="onChangeSearchInput"
+          @clear-search="onClearSearchInput"
+        />
+        <div class="ml-sm-4">
+          <table-cell-count
+            :filtered-items-count="filteredRows"
+            :total-number-of-cells="allLogs.length"
+          ></table-cell-count>
+        </div>
+      </b-col>
+      <b-col sm="8" md="7" xl="6">
+        <table-date-filter @change="onChangeDateTimeFilter" />
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col xl="12" class="text-right">
+        <b-button
+          variant="primary"
+          :disabled="allLogs.length === 0"
+          :download="exportFileNameByDate()"
+          :href="href"
+        >
+          <icon-export /> {{ $t('pagePostCodeLogs.button.exportAll') }}
+        </b-button>
+      </b-col>
+    </b-row>
+    <b-row>
+      <b-col>
+        <table-toolbar
+          ref="toolbar"
+          :selected-items-count="selectedRows.length"
+          @clear-selected="clearSelectedRows($refs.table)"
+        >
+          <template #export>
+            <table-toolbar-export
+              :data="batchExportData"
+              :file-name="exportFileNameByDate('export')"
+            />
+          </template>
+        </table-toolbar>
+        <b-table
+          id="table-post-code-logs"
+          ref="table"
+          responsive="md"
+          selectable
+          no-select-on-click
+          sort-icon-left
+          hover
+          no-sort-reset
+          sort-desc
+          show-empty
+          sort-by="id"
+          :fields="fields"
+          :items="filteredLogs"
+          :empty-text="$t('global.table.emptyMessage')"
+          :empty-filtered-text="$t('global.table.emptySearchMessage')"
+          :per-page="perPage"
+          :current-page="currentPage"
+          :filter="searchFilter"
+          @filtered="onFiltered"
+          @row-selected="onRowSelected($event, filteredLogs.length)"
+        >
+          <!-- Checkbox column -->
+          <template #head(checkbox)>
+            <b-form-checkbox
+              v-model="tableHeaderCheckboxModel"
+              data-test-id="postCode-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="`postCode-checkbox-selectRow-${row.index}`"
+              @change="toggleSelectRow($refs.table, row.index)"
+            >
+              <span class="sr-only">{{ $t('global.table.selectItem') }}</span>
+            </b-form-checkbox>
+          </template>
+          <!-- Date column -->
+          <template #cell(date)="{ value }">
+            <p class="mb-0">{{ value | formatDate }}</p>
+            <p class="mb-0">{{ value | formatTime }}</p>
+          </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"
+              :row-data="row.item"
+              :btn-icon-only="true"
+              :export-name="exportFileNameByDate(action.value)"
+              :download-location="row.item.uri"
+              :download-in-new-tab="true"
+            >
+              <template #icon>
+                <icon-export v-if="action.value === 'export'" />
+                <icon-download v-if="action.value === 'download'" />
+              </template>
+            </table-row-action>
+          </template>
+        </b-table>
+      </b-col>
+    </b-row>
+
+    <!-- Table pagination -->
+    <b-row>
+      <b-col sm="6">
+        <b-form-group
+          class="table-pagination-select"
+          :label="$t('global.table.itemsPerPage')"
+          label-for="pagination-items-per-page"
+        >
+          <b-form-select
+            id="pagination-items-per-page"
+            v-model="perPage"
+            :options="itemsPerPageOptions"
+          />
+        </b-form-group>
+      </b-col>
+      <b-col sm="6">
+        <b-pagination
+          v-model="currentPage"
+          first-number
+          last-number
+          :per-page="perPage"
+          :total-rows="getTotalRowCount(filteredLogs.length)"
+          aria-controls="table-post-code-logs"
+        />
+      </b-col>
+    </b-row>
+  </b-container>
+</template>
+
+<script>
+import IconDownload from '@carbon/icons-vue/es/download/20';
+import IconExport from '@carbon/icons-vue/es/document--export/20';
+import { omit } from 'lodash';
+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 TableToolbarExport from '@/components/Global/TableToolbarExport';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import TableFilterMixin from '@/components/Mixins/TableFilterMixin';
+import BVPaginationMixin, {
+  currentPage,
+  perPage,
+  itemsPerPageOptions,
+} from '@/components/Mixins/BVPaginationMixin';
+import BVTableSelectableMixin, {
+  selectedRows,
+  tableHeaderCheckboxModel,
+  tableHeaderCheckboxIndeterminate,
+} from '@/components/Mixins/BVTableSelectableMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import TableDataFormatterMixin from '@/components/Mixins/TableDataFormatterMixin';
+import TableSortMixin from '@/components/Mixins/TableSortMixin';
+import TableRowExpandMixin, {
+  expandRowLabel,
+} from '@/components/Mixins/TableRowExpandMixin';
+import SearchFilterMixin, {
+  searchFilter,
+} from '@/components/Mixins/SearchFilterMixin';
+
+export default {
+  components: {
+    IconExport,
+    IconDownload,
+    PageTitle,
+    Search,
+    TableCellCount,
+    TableRowAction,
+    TableToolbar,
+    TableToolbarExport,
+    TableDateFilter,
+  },
+  mixins: [
+    BVPaginationMixin,
+    BVTableSelectableMixin,
+    BVToastMixin,
+    LoadingBarMixin,
+    TableFilterMixin,
+    TableDataFormatterMixin,
+    TableSortMixin,
+    TableRowExpandMixin,
+    SearchFilterMixin,
+  ],
+  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: 'date',
+          label: this.$t('pagePostCodeLogs.table.created'),
+        },
+        {
+          key: 'timeStampOffset',
+          label: this.$t('pagePostCodeLogs.table.timeStampOffset'),
+        },
+        {
+          key: 'bootCount',
+          label: this.$t('pagePostCodeLogs.table.bootCount'),
+        },
+        {
+          key: 'postCode',
+          label: this.$t('pagePostCodeLogs.table.postCode'),
+        },
+        {
+          key: 'actions',
+          label: '',
+          tdClass: 'text-right text-nowrap',
+        },
+      ],
+      expandRowLabel,
+      activeFilters: [],
+      currentPage: currentPage,
+      filterStartDate: null,
+      filterEndDate: null,
+      itemsPerPageOptions: itemsPerPageOptions,
+      perPage: perPage,
+      searchFilter: searchFilter,
+      searchTotalFilteredRows: 0,
+      selectedRows: selectedRows,
+      tableHeaderCheckboxModel: tableHeaderCheckboxModel,
+      tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate,
+    };
+  },
+  computed: {
+    href() {
+      return `data:text/json;charset=utf-8,${this.exportAllLogsString()}`;
+    },
+    filteredRows() {
+      return this.searchFilter
+        ? this.searchTotalFilteredRows
+        : this.filteredLogs.length;
+    },
+    allLogs() {
+      return this.$store.getters['postCodeLogs/allPostCodes'].map(
+        (postCodes) => {
+          return {
+            ...postCodes,
+            actions: [
+              {
+                value: 'export',
+                title: this.$t('pagePostCodeLogs.action.exportLogs'),
+              },
+              {
+                value: 'download',
+                title: this.$t('pagePostCodeLogs.action.downloadDetails'),
+              },
+            ],
+          };
+        }
+      );
+    },
+    batchExportData() {
+      return this.selectedRows.map((row) => omit(row, 'actions'));
+    },
+    filteredLogsByDate() {
+      return this.getFilteredTableDataByDate(
+        this.allLogs,
+        this.filterStartDate,
+        this.filterEndDate
+      );
+    },
+    filteredLogs() {
+      return this.getFilteredTableData(
+        this.filteredLogsByDate,
+        this.activeFilters
+      );
+    },
+  },
+  created() {
+    this.startLoader();
+    this.$store
+      .dispatch('postCodeLogs/getPostCodesLogData')
+      .finally(() => this.endLoader());
+  },
+  methods: {
+    exportAllLogsString() {
+      {
+        return this.$store.getters['postCodeLogs/allPostCodes'].map(
+          (postCodes) => {
+            const allLogsString = JSON.stringify(postCodes);
+            return allLogsString;
+          }
+        );
+      }
+    },
+    onFilterChange({ activeFilters }) {
+      this.activeFilters = activeFilters;
+    },
+    onChangeDateTimeFilter({ fromDate, toDate }) {
+      this.filterStartDate = fromDate;
+      this.filterEndDate = toDate;
+    },
+    onFiltered(filteredItems) {
+      this.searchTotalFilteredRows = filteredItems.length;
+    },
+    // Create export file name based on date and action
+    exportFileNameByDate(value) {
+      let date = new Date();
+      date =
+        date.toISOString().slice(0, 10) +
+        '_' +
+        date.toString().split(':').join('-').split(' ')[4];
+      let fileName;
+      if (value === 'download') {
+        fileName = this.$t('pagePostCodeLogs.downloadFilePrefix');
+      } else if (value === 'export') {
+        fileName = this.$t('pagePostCodeLogs.exportFilePrefix');
+      } else {
+        fileName = this.$t('pagePostCodeLogs.allExportFilePrefix');
+      }
+      return fileName + date;
+    },
+  },
+};
+</script>
diff --git a/src/views/Health/PostCodeLogs/index.js b/src/views/Health/PostCodeLogs/index.js
new file mode 100644
index 0000000..ab59112
--- /dev/null
+++ b/src/views/Health/PostCodeLogs/index.js
@@ -0,0 +1,2 @@
+import PostCodeLogs from './PostCodeLogs.vue';
+export default PostCodeLogs;