Add date filter on Event logs page

Created global TableDateFilter component that uses the BootstrapVue
Datepicker with a native text input. This will allow users to manually
enter a date in ISO format or use the Bootstrap calendar dropdown.

Storing language preference from Login to use locale prop on
BootstrapVue Datepicker component.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I66de9fb04451572c9a90f90d8522934b6204aed2
diff --git a/src/assets/styles/_obmc-custom.scss b/src/assets/styles/_obmc-custom.scss
index f443799..63bed23 100644
--- a/src/assets/styles/_obmc-custom.scss
+++ b/src/assets/styles/_obmc-custom.scss
@@ -18,3 +18,4 @@
 
 // Vendor overrides must be the last file imported
 @import "./vendor-overrides/bootstrap/index";
+@import "./vendor-overrides/bootstrap-vue/index";
diff --git a/src/assets/styles/vendor-overrides/bootstrap-vue/_calendar.scss b/src/assets/styles/vendor-overrides/bootstrap-vue/_calendar.scss
new file mode 100644
index 0000000..bf7572e
--- /dev/null
+++ b/src/assets/styles/vendor-overrides/bootstrap-vue/_calendar.scss
@@ -0,0 +1,8 @@
+.b-calendar-nav {
+  .btn {
+    &:hover {
+      background: none;
+      color: $dark;
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/assets/styles/vendor-overrides/bootstrap-vue/_index.scss b/src/assets/styles/vendor-overrides/bootstrap-vue/_index.scss
new file mode 100644
index 0000000..a665814
--- /dev/null
+++ b/src/assets/styles/vendor-overrides/bootstrap-vue/_index.scss
@@ -0,0 +1 @@
+@import "./calendar";
diff --git a/src/components/Global/TableDateFilter.vue b/src/components/Global/TableDateFilter.vue
new file mode 100644
index 0000000..e73d7d5
--- /dev/null
+++ b/src/components/Global/TableDateFilter.vue
@@ -0,0 +1,176 @@
+<template>
+  <b-row class="mb-2">
+    <b-col class="d-flex">
+      <b-form-group
+        :label="$t('global.table.fromDate')"
+        label-for="input-from-date"
+        class="mr-3 my-0 w-100"
+      >
+        <b-input-group>
+          <b-form-input
+            id="input-from-date"
+            v-model="fromDate"
+            placeholder="YYYY-MM-DD"
+            :state="getValidationState($v.fromDate)"
+            @blur="$v.fromDate.$touch()"
+          />
+          <b-form-invalid-feedback role="alert">
+            <template v-if="!$v.fromDate.pattern">
+              {{ $t('global.form.invalidFormat') }}
+            </template>
+            <template v-if="!$v.fromDate.maxDate">
+              {{ $t('global.form.dateMustBeBefore', { date: toDate }) }}
+            </template>
+          </b-form-invalid-feedback>
+          <template slot:append>
+            <b-form-datepicker
+              v-model="fromDate"
+              button-only
+              right
+              size="sm"
+              :max="toDate"
+              :hide-header="true"
+              :locale="locale"
+              :label-help="
+                $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+              "
+              button-variant="link"
+              aria-controls="input-from-date"
+            >
+              <template v-slot:button-content>
+                <icon-calendar />
+                <span class="sr-only">{{
+                  $t('global.calendar.openDatePicker')
+                }}</span>
+              </template>
+            </b-form-datepicker>
+          </template>
+        </b-input-group>
+      </b-form-group>
+      <b-form-group
+        :label="$t('global.table.toDate')"
+        label-for="input-to-date"
+        class="my-0 w-100"
+      >
+        <b-input-group>
+          <b-form-input
+            id="input-to-date"
+            v-model="toDate"
+            placeholder="YYYY-MM-DD"
+            :state="getValidationState($v.toDate)"
+            @blur="$v.toDate.$touch()"
+          />
+          <b-form-invalid-feedback role="alert">
+            <template v-if="!$v.toDate.pattern">
+              {{ $t('global.form.invalidFormat') }}
+            </template>
+            <template v-if="!$v.toDate.minDate">
+              {{ $t('global.form.dateMustBeAfter', { date: fromDate }) }}
+            </template>
+          </b-form-invalid-feedback>
+          <template slot:append>
+            <b-form-datepicker
+              v-model="toDate"
+              button-only
+              right
+              size="sm"
+              :min="fromDate"
+              :hide-header="true"
+              :locale="locale"
+              :label-help="
+                $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+              "
+              button-variant="link"
+              aria-controls="input-to-date"
+            >
+              <template v-slot:button-content>
+                <icon-calendar />
+                <span class="sr-only">{{
+                  $t('global.calendar.openDatePicker')
+                }}</span>
+              </template>
+            </b-form-datepicker>
+          </template>
+        </b-input-group>
+      </b-form-group>
+    </b-col>
+  </b-row>
+</template>
+
+<script>
+import IconCalendar from '@carbon/icons-vue/es/calendar/20';
+import { helpers } from 'vuelidate/lib/validators';
+
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
+
+export default {
+  components: { IconCalendar },
+  mixins: [VuelidateMixin],
+  data() {
+    return {
+      fromDate: '',
+      toDate: '',
+      offsetToDate: '',
+      locale: this.$store.getters['global/languagePreference']
+    };
+  },
+  validations() {
+    return {
+      fromDate: {
+        pattern: helpers.regex('pattern', isoDateRegex),
+        maxDate: value => {
+          if (!this.toDate) return true;
+          const date = new Date(value);
+          const maxDate = new Date(this.toDate);
+          if (date.getTime() > maxDate.getTime()) return false;
+          return true;
+        }
+      },
+      toDate: {
+        pattern: helpers.regex('pattern', isoDateRegex),
+        minDate: value => {
+          if (!this.fromDate) return true;
+          const date = new Date(value);
+          const minDate = new Date(this.fromDate);
+          if (date.getTime() < minDate.getTime()) return false;
+          return true;
+        }
+      }
+    };
+  },
+  watch: {
+    fromDate() {
+      this.emitChange();
+    },
+    toDate(newVal) {
+      // Offset the end date to end of day to make sure all
+      // entries from selected end date are included in filter
+      this.offsetToDate = new Date(newVal).setUTCHours(23, 59, 59, 999);
+      this.emitChange();
+    }
+  },
+  methods: {
+    emitChange() {
+      if (this.$v.$invalid) return;
+      this.$v.$reset(); //reset to re-validate on blur
+      this.$emit('change', {
+        fromDate: this.fromDate ? new Date(this.fromDate) : null,
+        toDate: this.toDate ? new Date(this.offsetToDate) : null
+      });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import 'src/assets/styles/helpers';
+
+.b-form-datepicker {
+  position: absolute;
+  right: 0;
+  top: 0;
+  z-index: $zindex-dropdown + 1;
+}
+</style>
diff --git a/src/components/Mixins/TableFilterMixin.js b/src/components/Mixins/TableFilterMixin.js
index 25c7497..58e70c5 100644
--- a/src/components/Mixins/TableFilterMixin.js
+++ b/src/components/Mixins/TableFilterMixin.js
@@ -16,6 +16,25 @@
         }
         return returnRow;
       });
+    },
+    getFilteredTableDataByDate(
+      tableData = [],
+      startDate,
+      endDate,
+      propertyKey = 'date'
+    ) {
+      if (!startDate && !endDate) return tableData;
+      const startDateInMs = startDate ? startDate.getTime() : 0;
+      const endDateInMs = endDate
+        ? endDate.getTime()
+        : Number.POSITIVE_INFINITY;
+      return tableData.filter(row => {
+        const date = row[propertyKey];
+        if (!(date instanceof Date)) return;
+
+        const dateInMs = date.getTime();
+        if (dateInMs >= startDateInMs && dateInMs <= endDateInMs) return row;
+      });
     }
   }
 };
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 9fc64d0..8a722e3 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -22,7 +22,13 @@
       "showPassword": "Show password as plain text. Note: this will visually expose your password on the screen.",
       "tooltip": "Tooltip"
     },
+    "calendar": {
+      "openDatePicker": "Open date picker",
+      "useCursorKeysToNavigateCalendarDates" : "Use cursor keys to navigate calendar dates"
+    },
     "form": {
+      "dateMustBeAfter": "Date must be after %{date}",
+      "dateMustBeBefore": "Date must be before %{date}",
       "fieldRequired": "Field required",
       "invalidFormat": "Invalid format",
       "invalidValue": "Invalid value",
@@ -46,7 +52,9 @@
       "informational": "Informational"
     },
     "table": {
+      "fromDate": "From date",
       "itemsPerPage": "Items per page",
+      "toDate": "To date",
       "viewAll": "View all"
     }
   },
diff --git a/src/main.js b/src/main.js
index 6896ec2..0c6d5b0 100644
--- a/src/main.js
+++ b/src/main.js
@@ -11,6 +11,7 @@
   DropdownPlugin,
   FormPlugin,
   FormCheckboxPlugin,
+  FormDatepickerPlugin,
   FormFilePlugin,
   FormGroupPlugin,
   FormInputPlugin,
@@ -74,6 +75,7 @@
 Vue.use(DropdownPlugin);
 Vue.use(FormPlugin);
 Vue.use(FormCheckboxPlugin);
+Vue.use(FormDatepickerPlugin);
 Vue.use(FormFilePlugin);
 Vue.use(FormGroupPlugin);
 Vue.use(FormInputPlugin);
diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js
index 1327422..42e9e2b 100644
--- a/src/store/modules/GlobalStore.js
+++ b/src/store/modules/GlobalStore.js
@@ -30,16 +30,20 @@
   namespaced: true,
   state: {
     bmcTime: null,
-    hostStatus: 'unreachable'
+    hostStatus: 'unreachable',
+    languagePreference: localStorage.getItem('storedLanguage') || 'en-US'
   },
   getters: {
     hostStatus: state => state.hostStatus,
-    bmcTime: state => state.bmcTime
+    bmcTime: state => state.bmcTime,
+    languagePreference: state => state.languagePreference
   },
   mutations: {
     setBmcTime: (state, bmcTime) => (state.bmcTime = bmcTime),
     setHostStatus: (state, hostState) =>
-      (state.hostStatus = hostStateMapper(hostState))
+      (state.hostStatus = hostStateMapper(hostState)),
+    setLanguagePreference: (state, language) =>
+      (state.languagePreference = language)
   },
   actions: {
     async getBmcTime({ commit }) {
diff --git a/src/views/Health/EventLogs/EventLogs.vue b/src/views/Health/EventLogs/EventLogs.vue
index a5ef375..44a2485 100644
--- a/src/views/Health/EventLogs/EventLogs.vue
+++ b/src/views/Health/EventLogs/EventLogs.vue
@@ -1,6 +1,11 @@
 <template>
   <b-container fluid="xl">
     <page-title />
+    <b-row class="mb-3">
+      <b-col md="6" lg="7" xl="5" offset-md="6" offset-lg="5" offset-xl="7">
+        <table-date-filter @change="onChangeDateTimeFilter" />
+      </b-col>
+    </b-row>
     <b-row>
       <b-col class="text-right">
         <table-filter :filters="tableFilters" @filterChange="onFilterChange" />
@@ -121,6 +126,7 @@
 
 import PageTitle from '@/components/Global/PageTitle';
 import StatusIcon from '@/components/Global/StatusIcon';
+import TableDateFilter from '@/components/Global/TableDateFilter';
 import TableFilter from '@/components/Global/TableFilter';
 import TableRowAction from '@/components/Global/TableRowAction';
 import TableToolbar from '@/components/Global/TableToolbar';
@@ -143,7 +149,8 @@
     TableFilter,
     TableRowAction,
     TableToolbar,
-    TableToolbarExport
+    TableToolbarExport,
+    TableDateFilter
   },
   mixins: [
     BVPaginationMixin,
@@ -202,7 +209,9 @@
           value: 'delete',
           label: this.$t('global.action.delete')
         }
-      ]
+      ],
+      filterStartDate: null,
+      filterEndDate: null
     };
   },
   computed: {
@@ -223,16 +232,21 @@
         };
       });
     },
-    filteredLogs: {
-      get: function() {
-        return this.getFilteredTableData(this.allLogs, this.activeFilters);
-      },
-      set: function(newVal) {
-        return newVal;
-      }
-    },
     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() {
@@ -273,10 +287,6 @@
     },
     onFilterChange({ activeFilters }) {
       this.activeFilters = activeFilters;
-      this.filteredLogs = this.getFilteredTableData(
-        this.allLogs,
-        activeFilters
-      );
     },
     onSortCompare(a, b, key) {
       if (key === 'severity') {
@@ -316,6 +326,10 @@
             if (deleteConfirmed) this.deleteLogs(uris);
           });
       }
+    },
+    onChangeDateTimeFilter({ fromDate, toDate }) {
+      this.filterStartDate = fromDate;
+      this.filterEndDate = toDate;
     }
   }
 };
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
index d373be2..4d8f248 100644
--- a/src/views/Login/Login.vue
+++ b/src/views/Login/Login.vue
@@ -136,7 +136,10 @@
       this.$store
         .dispatch('authentication/login', [username, password])
         .then(() => this.$router.push('/'))
-        .then(localStorage.setItem('storedLanguage', i18n.locale))
+        .then(() => {
+          localStorage.setItem('storedLanguage', i18n.locale);
+          this.$store.commit('global/setLanguagePreference', i18n.locale);
+        })
         .catch(error => console.log(error))
         .finally(() => (this.disableSubmitButton = false));
     }