Add date and time settings

Adds ablity to change date and time manually, or
configure using NTP servers.

- If NTP is selected, user is required to enter at least one
  NTP address
- Date and time are ISO formatted

Signed-off-by: Dixsie Wolmers <dixsie@ibm.com>
Change-Id: I0d67c80487fdd815eacc3539ccd702b23618260e
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index 1dfba11..175b0aa 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -59,6 +59,9 @@
               <icon-expand class="icon-expand" />
             </b-button>
             <b-collapse id="configuration-menu" tag="ul" class="nav-item__nav">
+              <b-nav-item to="/configuration/date-time-settings">
+                {{ $t('appNavigation.dateTimeSettings') }}
+              </b-nav-item>
               <b-nav-item href="javascript:void(0)">
                 {{ $t('appNavigation.firmware') }}
               </b-nav-item>
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index f0494c7..2a41804 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -25,7 +25,7 @@
     },
     "calendar": {
       "openDatePicker": "Open date picker",
-      "useCursorKeysToNavigateCalendarDates" : "Use cursor keys to navigate calendar dates"
+      "useCursorKeysToNavigateCalendarDates": "Use cursor keys to navigate calendar dates"
     },
     "form": {
       "dateMustBeAfter": "Date must be after %{date}",
@@ -73,6 +73,7 @@
     "accessControl": "Access Control",
     "configuration": "Configuration",
     "control": "Control",
+    "dateTimeSettings": "@:appPageTitle.dateTimeSettings",
     "eventLogs": "@:appPageTitle.eventLogs",
     "firmware": "@:appPageTitle.firmware",
     "hardwareStatus": "@:appPageTitle.hardwareStatus",
@@ -92,6 +93,7 @@
     "sslCertificates": "@:appPageTitle.sslCertificates"
   },
   "appPageTitle": {
+    "dateTimeSettings": "Date and time settings",
     "eventLogs": "Event logs",
     "firmware": "Firmware",
     "hardwareStatus": "Hardware status",
@@ -101,7 +103,7 @@
     "managePowerUsage": "Manage power usage",
     "networkSettings": "Network settings",
     "overview": "Overview",
-    "profileSettings":"Profile settings",
+    "profileSettings": "Profile settings",
     "rebootBmc": "Reboot BMC",
     "sensors": "Sensors",
     "serialOverLan": "Serial over LAN console",
@@ -111,6 +113,27 @@
     "sslCertificates": "SSL Certificates",
     "unauthorized": "Unauthorized"
   },
+  "pageDateTimeSettings": {
+    "alert": {
+      "message": "To change how date and time are displayed (either UTC or browser offset) throughout the application, visit ",
+      "link": "Profile Settings"
+     },
+    "configureSettings": "Configure settings",
+    "form": {
+      "date": "Date",
+      "manual": "Manual",
+      "time": "Time",
+      "ntpServers": {
+        "server1": "Server 1",
+        "server2": "Server 2",
+        "server3": "Server 3"
+      }
+    },
+    "toast": {
+      "errorSaveDateTimeSettings": "Error saving date and time settings.",
+      "successSaveDateTimeSettings": "Successfully saved date and time settings."
+    }
+  },
   "pageEventLogs": {
     "modal": {
       "deleteTitle": "Delete log | Delete logs",
@@ -138,30 +161,30 @@
     "chassis": "Chassis",
     "system": "System",
     "table": {
-        "assetTag": "Asset tag",
-        "chassisType": "Chassis type",
-        "connectTypesSupported": "Connect types supported",
-        "description": "Description",
-        "efficiencyPercent": "Efficiency percent",
-        "firmwareVersion": "Firmware version",
-        "graphicalConsole": "Graphical console",
-        "health": "Health",
-        "id": "ID",
-        "indicatorLed": "Indicator LED",
-        "manufacturer": "Manufacturer",
-        "maxConcurrentSessions": "Max concurrent sessions",
-        "model": "Model",
-        "partNumber": "Part number",
-        "powerInputWatts": "Power input watts",
-        "powerState": "Power state",
-        "serialConsole": "Serial console",
-        "serialNumber": "Serial number",
-        "serviceEnabled": "Service enabled",
-        "serviceEntryPointUuid": "Service entry point UUID",
-        "statusHealthRollup": "Status (Health rollup)",
-        "statusState": "Status (State)",
-        "systemType": "System type",
-        "uuid": "UUID"
+      "assetTag": "Asset tag",
+      "chassisType": "Chassis type",
+      "connectTypesSupported": "Connect types supported",
+      "description": "Description",
+      "efficiencyPercent": "Efficiency percent",
+      "firmwareVersion": "Firmware version",
+      "graphicalConsole": "Graphical console",
+      "health": "Health",
+      "id": "ID",
+      "indicatorLed": "Indicator LED",
+      "manufacturer": "Manufacturer",
+      "maxConcurrentSessions": "Max concurrent sessions",
+      "model": "Model",
+      "partNumber": "Part number",
+      "powerInputWatts": "Power input watts",
+      "powerState": "Power state",
+      "serialConsole": "Serial console",
+      "serialNumber": "Serial number",
+      "serviceEnabled": "Service enabled",
+      "serviceEntryPointUuid": "Service entry point UUID",
+      "statusHealthRollup": "Status (Health rollup)",
+      "statusState": "Status (State)",
+      "systemType": "System type",
+      "uuid": "UUID"
     }
   },
   "pageLdap": {
diff --git a/src/router/index.js b/src/router/index.js
index a3d2806..3d8c646 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -82,6 +82,14 @@
         }
       },
       {
+        path: '/configuration/date-time-settings',
+        name: 'date-time-settings',
+        component: () => import('@/views/Configuration/DateTimeSettings'),
+        meta: {
+          title: 'appPageTitle.dateTimeSettings'
+        }
+      },
+      {
         path: '/control/manage-power-usage',
         name: 'manage-power-usage',
         component: () => import('@/views/Control/ManagePowerUsage'),
diff --git a/src/store/index.js b/src/store/index.js
index 6ad0539..392344d 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -22,6 +22,7 @@
 import BmcStore from './modules/Health/BmcStore';
 
 import WebSocketPlugin from './plugins/WebSocketPlugin';
+import DateTimeStore from './modules/Configuration/DateTimeSettingsStore';
 
 Vue.use(Vuex);
 
@@ -32,6 +33,7 @@
   modules: {
     global: GlobalStore,
     authentication: AuthenticationStore,
+    dateTime: DateTimeStore,
     ldap: LdapStore,
     localUsers: LocalUserManagementStore,
     firmware: FirmwareStore,
diff --git a/src/store/modules/Configuration/DateTimeSettingsStore.js b/src/store/modules/Configuration/DateTimeSettingsStore.js
new file mode 100644
index 0000000..9da0cb4
--- /dev/null
+++ b/src/store/modules/Configuration/DateTimeSettingsStore.js
@@ -0,0 +1,68 @@
+import api from '../../api';
+import i18n from '@/i18n';
+
+const DateTimeStore = {
+  namespaced: true,
+  state: {
+    ntpServers: [],
+    isNtpProtocolEnabled: null
+  },
+  getters: {
+    ntpServers: state => state.ntpServers,
+    isNtpProtocolEnabled: state => state.isNtpProtocolEnabled
+  },
+  mutations: {
+    setNtpServers: (state, ntpServers) => (state.ntpServers = ntpServers),
+    setIsNtpProtocolEnabled: (state, isNtpProtocolEnabled) =>
+      (state.isNtpProtocolEnabled = isNtpProtocolEnabled)
+  },
+  actions: {
+    async getNtpData({ commit }) {
+      return await api
+        .get('/redfish/v1/Managers/bmc/NetworkProtocol')
+        .then(response => {
+          const ntpServers = response.data.NTP.NTPServers;
+          const isNtpProtocolEnabled = response.data.NTP.ProtocolEnabled;
+          commit('setNtpServers', ntpServers);
+          commit('setIsNtpProtocolEnabled', isNtpProtocolEnabled);
+        })
+        .catch(error => {
+          console.log(error);
+        });
+    },
+    async updateDateTimeSettings(_, dateTimeForm) {
+      const ntpData = {
+        NTP: {
+          ProtocolEnabled: dateTimeForm.ntpProtocolEnabled
+        }
+      };
+
+      if (dateTimeForm.ntpProtocolEnabled) {
+        ntpData.NTP.NTPServers = dateTimeForm.ntpServersArray;
+      }
+      return await api
+        .patch(`/redfish/v1/Managers/bmc/NetworkProtocol`, ntpData)
+        .then(() => {
+          if (!dateTimeForm.ntpProtocolEnabled) {
+            const dateTimeData = {
+              DateTime: dateTimeForm.updatedDateTime
+            };
+            api.patch(`/redfish/v1/Managers/bmc`, dateTimeData);
+          }
+        })
+        .then(() => {
+          return i18n.t(
+            'pageDateTimeSettings.toast.successSaveDateTimeSettings'
+          );
+        })
+        .catch(error => {
+          console.log(error);
+          throw new Error(
+            i18n.t('pageDateTimeSettings.toast.errorSaveDateTimeSettings')
+          );
+        });
+    }
+  }
+};
+
+export default DateTimeStore;
diff --git a/src/views/Configuration/DateTimeSettings/DateTimeSettings.vue b/src/views/Configuration/DateTimeSettings/DateTimeSettings.vue
new file mode 100644
index 0000000..d7b97d1
--- /dev/null
+++ b/src/views/Configuration/DateTimeSettings/DateTimeSettings.vue
@@ -0,0 +1,352 @@
+<template>
+  <b-container fluid="xl">
+    <page-title />
+    <b-row>
+      <b-col md="8" xl="6">
+        <alert variant="info" class="mb-4">
+          <span>
+            {{ $t('pageDateTimeSettings.alert.message') }}
+            <b-link to="/profile-settings">
+              {{ $t('pageDateTimeSettings.alert.link') }}</b-link
+            >
+          </span>
+        </alert>
+      </b-col>
+    </b-row>
+    <page-section>
+      <b-row>
+        <b-col lg="3">
+          <dl>
+            <dt>{{ $t('pageDateTimeSettings.form.date') }}</dt>
+            <dd v-if="bmcTime">{{ bmcTime | formatDate }}</dd>
+            <dd v-else>--</dd>
+          </dl>
+        </b-col>
+        <b-col lg="3">
+          <dl>
+            <dt>{{ $t('pageDateTimeSettings.form.time') }}</dt>
+            <dd v-if="bmcTime">{{ bmcTime | formatTime }}</dd>
+            <dd v-else>--</dd>
+          </dl>
+        </b-col>
+      </b-row>
+    </page-section>
+    <page-section :section-title="$t('pageDateTimeSettings.configureSettings')">
+      <b-form novalidate @submit.prevent="submitForm">
+        <b-form-group label="Configure date and time" label-sr-only>
+          <b-form-radio
+            v-model="form.configurationSelected"
+            value="manual"
+            @change="onChangeConfigType"
+          >
+            {{ $t('pageDateTimeSettings.form.manual') }}
+          </b-form-radio>
+          <b-row class="mt-3 ml-3">
+            <b-col sm="6" lg="4" xl="3">
+              <b-form-group
+                :label="$t('pageDateTimeSettings.form.date')"
+                label-for="input-manual-date"
+              >
+                <b-form-text id="date-format-help">(YYYY-MM-DD)</b-form-text>
+                <b-input-group>
+                  <b-form-input
+                    id="input-manual-date"
+                    v-model="form.manual.date"
+                    :state="getValidationState($v.form.manual.date)"
+                    :disabled="form.configurationSelected === 'ntp'"
+                    @blur="$v.form.manual.date.$touch()"
+                  />
+                  <b-form-invalid-feedback role="alert">
+                    <div v-if="!$v.form.manual.date.pattern">
+                      {{ $t('global.form.invalidFormat') }}
+                    </div>
+                    <div v-if="!$v.form.manual.time.required">
+                      {{ $t('global.form.fieldRequired') }}
+                    </div>
+                  </b-form-invalid-feedback>
+                  <b-form-datepicker
+                    v-model="form.manual.date"
+                    button-only
+                    right
+                    size="sm"
+                    :hide-header="true"
+                    :locale="locale"
+                    :label-help="
+                      $t('global.calendar.useCursorKeysToNavigateCalendarDates')
+                    "
+                    :disabled="form.configurationSelected === 'ntp'"
+                    button-variant="link"
+                    aria-controls="input-manual-date"
+                  >
+                    <template v-slot:button-content>
+                      <icon-calendar />
+                      <span class="sr-only">
+                        {{ $t('global.calendar.openDatePicker') }}
+                      </span>
+                    </template>
+                  </b-form-datepicker>
+                </b-input-group>
+              </b-form-group>
+            </b-col>
+            <b-col sm="6" lg="4" xl="3">
+              <b-form-group
+                :label="$t('pageDateTimeSettings.form.time')"
+                label-for="input-manual-time"
+              >
+                <b-form-text id="time-format-help">(HH:MM)</b-form-text>
+                <b-input-group>
+                  <b-form-input
+                    id="input-manual-time"
+                    v-model="form.manual.time"
+                    :state="getValidationState($v.form.manual.time)"
+                    :disabled="form.configurationSelected === 'ntp'"
+                    @blur="$v.form.manual.time.$touch()"
+                  />
+                  <b-form-invalid-feedback role="alert">
+                    <div v-if="!$v.form.manual.time.pattern">
+                      {{ $t('global.form.invalidFormat') }}
+                    </div>
+                    <div v-if="!$v.form.manual.time.required">
+                      {{ $t('global.form.fieldRequired') }}
+                    </div>
+                  </b-form-invalid-feedback>
+                </b-input-group>
+              </b-form-group>
+            </b-col>
+          </b-row>
+          <b-form-radio
+            v-model="form.configurationSelected"
+            value="ntp"
+            @change="onChangeConfigType"
+          >
+            NTP
+          </b-form-radio>
+          <b-row class="mt-3 ml-3">
+            <b-col sm="6" lg="4" xl="3">
+              <b-form-group
+                :label="$t('pageDateTimeSettings.form.ntpServers.server1')"
+                label-for="input-ntp-1"
+              >
+                <b-input-group>
+                  <b-form-input
+                    id="input-ntp-1"
+                    v-model="form.ntp.firstAddress"
+                    :state="getValidationState($v.form.ntp.firstAddress)"
+                    :disabled="form.configurationSelected === 'manual'"
+                    @blur="$v.form.ntp.firstAddress.$touch()"
+                  />
+                  <b-form-invalid-feedback role="alert">
+                    <div v-if="!$v.form.ntp.firstAddress.required">
+                      {{ $t('global.form.fieldRequired') }}
+                    </div>
+                  </b-form-invalid-feedback>
+                </b-input-group>
+              </b-form-group>
+            </b-col>
+            <b-col sm="6" lg="4" xl="3">
+              <b-form-group
+                :label="$t('pageDateTimeSettings.form.ntpServers.server2')"
+                label-for="input-ntp-2"
+              >
+                <b-input-group>
+                  <b-form-input
+                    id="input-ntp-2"
+                    v-model="form.ntp.secondAddress"
+                    :disabled="form.configurationSelected === 'manual'"
+                    @blur="$v.form.ntp.secondAddress.$touch()"
+                  />
+                </b-input-group>
+              </b-form-group>
+            </b-col>
+            <b-col sm="6" lg="4" xl="3">
+              <b-form-group
+                :label="$t('pageDateTimeSettings.form.ntpServers.server3')"
+                label-for="input-ntp-3"
+              >
+                <b-input-group>
+                  <b-form-input
+                    id="input-ntp-3"
+                    v-model="form.ntp.thirdAddress"
+                    :disabled="form.configurationSelected === 'manual'"
+                    @blur="$v.form.ntp.thirdAddress.$touch()"
+                  />
+                </b-input-group>
+              </b-form-group>
+            </b-col>
+          </b-row>
+        </b-form-group>
+        <b-button variant="primary" type="submit">
+          {{ $t('global.action.saveSettings') }}
+        </b-button>
+      </b-form>
+    </page-section>
+  </b-container>
+</template>
+
+<script>
+import Alert from '@/components/Global/Alert';
+import IconCalendar from '@carbon/icons-vue/es/calendar/20';
+import PageTitle from '@/components/Global/PageTitle';
+import PageSection from '@/components/Global/PageSection';
+
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+
+import { mapState } from 'vuex';
+import { requiredIf, helpers } from 'vuelidate/lib/validators';
+
+const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
+const isoTimeRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/;
+
+export default {
+  name: 'DateTimeSettings',
+  components: { Alert, IconCalendar, PageTitle, PageSection },
+  mixins: [BVToastMixin, LoadingBarMixin, VuelidateMixin],
+  data() {
+    return {
+      locale: this.$store.getters['global/languagePreference'],
+      form: {
+        configurationSelected: '',
+        manual: {
+          date: '',
+          time: ''
+        },
+        ntp: { firstAddress: '', secondAddress: '', thirdAddress: '' }
+      }
+    };
+  },
+  validations() {
+    return {
+      form: {
+        manual: {
+          date: {
+            required: requiredIf(function() {
+              return this.form.configurationSelected === 'manual';
+            }),
+            pattern: helpers.regex('pattern', isoDateRegex)
+          },
+          time: {
+            required: requiredIf(function() {
+              return this.form.configurationSelected === 'manual';
+            }),
+            pattern: helpers.regex('pattern', isoTimeRegex)
+          }
+        },
+        ntp: {
+          firstAddress: {
+            required: requiredIf(function() {
+              return this.form.configurationSelected === 'ntp';
+            })
+          },
+          secondAddress: {},
+          thirdAddress: {}
+        }
+      }
+    };
+  },
+  computed: {
+    ...mapState('dateTime', ['ntpServers', 'isNtpProtocolEnabled']),
+    bmcTime() {
+      return this.$store.getters['global/bmcTime'];
+    }
+  },
+  watch: {
+    ntpServers() {
+      this.setNtpValues();
+    },
+    manualDate() {
+      this.emitChange();
+    }
+  },
+  created() {
+    this.startLoader();
+    Promise.all([
+      this.$store.dispatch('global/getBmcTime'),
+      this.$store.dispatch('dateTime/getNtpData')
+    ]).finally(() => this.endLoader());
+  },
+  beforeRouteLeave(to, from, next) {
+    this.hideLoader();
+    next();
+  },
+  methods: {
+    emitChange() {
+      if (this.$v.$invalid) return;
+      this.$v.$reset(); //reset to re-validate on blur
+      this.$emit('change', {
+        manualDate: this.manualDate ? new Date(this.manualDate) : null
+      });
+    },
+    setNtpValues() {
+      this.form.configurationSelected = this.isNtpProtocolEnabled
+        ? 'ntp'
+        : 'manual';
+      this.form.ntp.firstAddress = this.ntpServers[0] || '';
+      this.form.ntp.secondAddress = this.ntpServers[1] || '';
+      this.form.ntp.thirdAddress = this.ntpServers[2] || '';
+    },
+    onChangeConfigType() {
+      this.$v.form.$reset();
+      this.setNtpValues();
+    },
+    submitForm() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.startLoader();
+
+      let dateTimeForm = {};
+      let ntpFirstAddress;
+      let ntpSecondAddress;
+      let ntpThirdAddress;
+      let isNTPEnabled = this.form.configurationSelected === 'ntp';
+
+      if (!isNTPEnabled) {
+        dateTimeForm.ntpProtocolEnabled = false;
+        dateTimeForm.updatedDateTime = new Date(
+          `${this.form.manual.date} ${this.form.manual.time}`
+        ).toISOString();
+      } else {
+        ntpFirstAddress = this.form.ntp.firstAddress;
+        ntpSecondAddress = this.form.ntp.secondAddress;
+        ntpThirdAddress = this.form.ntp.thirdAddress;
+        dateTimeForm.ntpProtocolEnabled = true;
+        dateTimeForm.ntpServersArray = [
+          ntpFirstAddress,
+          ntpSecondAddress,
+          ntpThirdAddress
+        ];
+      }
+
+      this.$store
+        .dispatch('dateTime/updateDateTimeSettings', dateTimeForm)
+        .then(success => {
+          this.successToast(success);
+          if (!isNTPEnabled) return;
+          // Shift address up if second address is empty
+          // to avoid refreshing after delay when updating NTP
+          if (ntpSecondAddress === '' && ntpThirdAddress !== '') {
+            this.form.ntp.secondAddress = ntpThirdAddress;
+            this.form.ntp.thirdAddress = '';
+          }
+        })
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => {
+          this.$v.form.$reset();
+          this.endLoader();
+        });
+    }
+  }
+};
+</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/views/Configuration/DateTimeSettings/index.js b/src/views/Configuration/DateTimeSettings/index.js
new file mode 100644
index 0000000..c8b5c08
--- /dev/null
+++ b/src/views/Configuration/DateTimeSettings/index.js
@@ -0,0 +1,2 @@
+import DateTimeSettings from './DateTimeSettings.vue';
+export default DateTimeSettings;