xref: /openbmc/webui-vue/src/views/Settings/DateTime/DateTime.vue (revision b44986729febcefc75a669d16dda5d081473b3aa)
1<template>
2  <b-container fluid="xl">
3    <page-title />
4    <b-row>
5      <b-col md="8" xl="6">
6        <alert variant="info" class="mb-4">
7          <span>
8            {{ $t('pageDateTime.alert.message') }}
9            <b-link to="/profile-settings">
10              {{ $t('pageDateTime.alert.link') }}</b-link
11            >
12          </span>
13        </alert>
14      </b-col>
15    </b-row>
16    <page-section>
17      <b-row>
18        <b-col lg="3">
19          <dl>
20            <dt>{{ $t('pageDateTime.form.date') }}</dt>
21            <dd v-if="bmcTime">{{ $filters.formatDate(bmcTime) }}</dd>
22            <dd v-else>--</dd>
23          </dl>
24        </b-col>
25        <b-col lg="3">
26          <dl>
27            <dt>{{ $t('pageDateTime.form.time.label') }}</dt>
28            <dd v-if="bmcTime">{{ $filters.formatTime(bmcTime) }}</dd>
29            <dd v-else>--</dd>
30          </dl>
31        </b-col>
32      </b-row>
33    </page-section>
34    <page-section :section-title="$t('pageDateTime.configureSettings')">
35      <b-form novalidate @submit.prevent="submitForm">
36        <b-form-group
37          label="Configure date and time"
38          :disabled="loading"
39          label-sr-only
40        >
41          <b-form-radio
42            v-model="form.configurationSelected"
43            value="manual"
44            data-test-id="dateTime-radio-configureManual"
45          >
46            {{ $t('pageDateTime.form.manual') }}
47          </b-form-radio>
48          <b-row class="mt-3 ml-3">
49            <b-col sm="6" lg="4" xl="3">
50              <b-form-group
51                :label="$t('pageDateTime.form.date')"
52                label-for="input-manual-date"
53              >
54                <b-form-text id="date-format-help">YYYY-MM-DD</b-form-text>
55                <b-input-group>
56                  <b-form-input
57                    id="input-manual-date"
58                    v-model="form.manual.date"
59                    :state="getValidationState(v$.form.manual.date)"
60                    :disabled="ntpOptionSelected"
61                    data-test-id="dateTime-input-manualDate"
62                    class="form-control-with-button"
63                    @blur="v$.form.manual.date.$touch()"
64                  />
65                  <b-form-invalid-feedback role="alert">
66                    <div v-if="v$.form.manual.date.pattern.$invalid">
67                      {{ $t('global.form.invalidFormat') }}
68                    </div>
69                    <div v-if="v$.form.manual.date.required.$invalid">
70                      {{ $t('global.form.fieldRequired') }}
71                    </div>
72                  </b-form-invalid-feedback>
73                  <b-form-datepicker
74                    v-model="form.manual.date"
75                    class="btn-datepicker btn-icon-only"
76                    button-only
77                    right
78                    :hide-header="true"
79                    :locale="locale"
80                    :label-help="
81                      $t('global.calendar.useCursorKeysToNavigateCalendarDates')
82                    "
83                    :title="$t('global.calendar.selectDate')"
84                    :disabled="ntpOptionSelected"
85                    button-variant="link"
86                    aria-controls="input-manual-date"
87                  >
88                    <template #button-content>
89                      <icon-calendar />
90                      <span class="sr-only">
91                        {{ $t('global.calendar.selectDate') }}
92                      </span>
93                    </template>
94                  </b-form-datepicker>
95                </b-input-group>
96              </b-form-group>
97            </b-col>
98            <b-col sm="6" lg="4" xl="3">
99              <b-form-group
100                :label="$t('pageDateTime.form.time.timezone', { timezone })"
101                label-for="input-manual-time"
102              >
103                <b-form-text id="time-format-help">HH:MM</b-form-text>
104                <b-input-group>
105                  <b-form-input
106                    id="input-manual-time"
107                    v-model="form.manual.time"
108                    :state="getValidationState(v$.form.manual.time)"
109                    :disabled="ntpOptionSelected"
110                    data-test-id="dateTime-input-manualTime"
111                    @blur="v$.form.manual.time.$touch()"
112                  />
113                  <b-form-invalid-feedback role="alert">
114                    <div v-if="v$.form.manual.time.pattern.$invalid">
115                      {{ $t('global.form.invalidFormat') }}
116                    </div>
117                    <div v-if="v$.form.manual.time.required.$invalid">
118                      {{ $t('global.form.fieldRequired') }}
119                    </div>
120                  </b-form-invalid-feedback>
121                </b-input-group>
122              </b-form-group>
123            </b-col>
124          </b-row>
125          <b-form-radio
126            v-model="form.configurationSelected"
127            value="ntp"
128            data-test-id="dateTime-radio-configureNTP"
129          >
130            NTP
131          </b-form-radio>
132          <b-row class="mt-3 ml-3">
133            <b-col sm="6" lg="4" xl="3">
134              <b-form-group
135                :label="$t('pageDateTime.form.ntpServers.server1')"
136                label-for="input-ntp-1"
137              >
138                <b-input-group>
139                  <b-form-input
140                    id="input-ntp-1"
141                    v-model="form.ntp.firstAddress"
142                    :state="getValidationState(v$.form.ntp.firstAddress)"
143                    :disabled="manualOptionSelected"
144                    data-test-id="dateTime-input-ntpServer1"
145                    @blur="v$.form.ntp.firstAddress.$touch()"
146                  />
147                  <b-form-invalid-feedback role="alert">
148                    <div v-if="v$.form.ntp.firstAddress.required.$invalid">
149                      {{ $t('global.form.fieldRequired') }}
150                    </div>
151                  </b-form-invalid-feedback>
152                </b-input-group>
153              </b-form-group>
154            </b-col>
155            <b-col sm="6" lg="4" xl="3">
156              <b-form-group
157                :label="$t('pageDateTime.form.ntpServers.server2')"
158                label-for="input-ntp-2"
159              >
160                <b-input-group>
161                  <b-form-input
162                    id="input-ntp-2"
163                    v-model="form.ntp.secondAddress"
164                    :disabled="manualOptionSelected"
165                    data-test-id="dateTime-input-ntpServer2"
166                  />
167                </b-input-group>
168              </b-form-group>
169            </b-col>
170            <b-col sm="6" lg="4" xl="3">
171              <b-form-group
172                :label="$t('pageDateTime.form.ntpServers.server3')"
173                label-for="input-ntp-3"
174              >
175                <b-input-group>
176                  <b-form-input
177                    id="input-ntp-3"
178                    v-model="form.ntp.thirdAddress"
179                    :disabled="manualOptionSelected"
180                    data-test-id="dateTime-input-ntpServer3"
181                  />
182                </b-input-group>
183              </b-form-group>
184            </b-col>
185          </b-row>
186          <b-button
187            variant="primary"
188            type="submit"
189            data-test-id="dateTime-button-saveSettings"
190          >
191            {{ $t('global.action.saveSettings') }}
192          </b-button>
193        </b-form-group>
194      </b-form>
195    </page-section>
196  </b-container>
197</template>
198
199<script>
200import Alert from '@/components/Global/Alert';
201import IconCalendar from '@carbon/icons-vue/es/calendar/20';
202import PageTitle from '@/components/Global/PageTitle';
203import PageSection from '@/components/Global/PageSection';
204
205import BVToastMixin from '@/components/Mixins/BVToastMixin';
206import LoadingBarMixin, { loading } from '@/components/Mixins/LoadingBarMixin';
207import LocalTimezoneLabelMixin from '@/components/Mixins/LocalTimezoneLabelMixin';
208import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
209import { useVuelidate } from '@vuelidate/core';
210
211import { mapState } from 'vuex';
212import { requiredIf } from '@vuelidate/validators';
213import { helpers } from 'vuelidate/lib/validators';
214import { useI18n } from 'vue-i18n';
215
216const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
217const isoTimeRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/;
218
219export default {
220  name: 'DateTime',
221  components: { Alert, IconCalendar, PageTitle, PageSection },
222  mixins: [
223    BVToastMixin,
224    LoadingBarMixin,
225    LocalTimezoneLabelMixin,
226    VuelidateMixin,
227  ],
228  beforeRouteLeave(to, from, next) {
229    this.hideLoader();
230    next();
231  },
232  setup() {
233    return {
234      v$: useVuelidate(),
235    };
236  },
237  data() {
238    return {
239      $t: useI18n().t,
240      locale: this.$store.getters['global/languagePreference'],
241      form: {
242        configurationSelected: 'manual',
243        manual: {
244          date: '',
245          time: '',
246        },
247        ntp: { firstAddress: '', secondAddress: '', thirdAddress: '' },
248      },
249      loading,
250    };
251  },
252  validations() {
253    return {
254      form: {
255        manual: {
256          date: {
257            required: requiredIf(function () {
258              return this.form.configurationSelected === 'manual';
259            }),
260            pattern: helpers.regex('pattern', isoDateRegex),
261          },
262          time: {
263            required: requiredIf(function () {
264              return this.form.configurationSelected === 'manual';
265            }),
266            pattern: helpers.regex('pattern', isoTimeRegex),
267          },
268        },
269        ntp: {
270          firstAddress: {
271            required: requiredIf(function () {
272              return this.form.configurationSelected === 'ntp';
273            }),
274          },
275        },
276      },
277    };
278  },
279  computed: {
280    ...mapState('dateTime', ['ntpServers', 'isNtpProtocolEnabled']),
281    bmcTime() {
282      return this.$store.getters['global/bmcTime'];
283    },
284    ntpOptionSelected() {
285      return this.form.configurationSelected === 'ntp';
286    },
287    manualOptionSelected() {
288      return this.form.configurationSelected === 'manual';
289    },
290    isUtcDisplay() {
291      return this.$store.getters['global/isUtcDisplay'];
292    },
293    timezone() {
294      if (this.isUtcDisplay) {
295        return 'UTC';
296      }
297      return this.localOffset();
298    },
299  },
300  watch: {
301    ntpServers() {
302      this.setNtpValues();
303    },
304    manualDate() {
305      this.emitChange();
306    },
307    bmcTime() {
308      this.form.manual.date = this.$filters.formatDate(
309        this.$store.getters['global/bmcTime'],
310      );
311      this.form.manual.time = this.$filters
312        .formatTime(this.$store.getters['global/bmcTime'])
313        .slice(0, 5);
314    },
315  },
316  created() {
317    this.startLoader();
318    this.setNtpValues();
319    Promise.all([
320      this.$store.dispatch('global/getBmcTime'),
321      this.$store.dispatch('dateTime/getNtpData'),
322    ]).finally(() => this.endLoader());
323  },
324  methods: {
325    emitChange() {
326      if (this.v$.$invalid) return;
327      this.v$.$reset(); //reset to re-validate on blur
328      this.$emit('change', {
329        manualDate: this.manualDate ? new Date(this.manualDate) : null,
330      });
331    },
332    setNtpValues() {
333      this.form.configurationSelected = this.isNtpProtocolEnabled
334        ? 'ntp'
335        : 'manual';
336      [
337        this.form.ntp.firstAddress = '',
338        this.form.ntp.secondAddress = '',
339        this.form.ntp.thirdAddress = '',
340      ] = [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]];
341    },
342    submitForm() {
343      this.v$.$touch();
344      if (this.v$.$invalid) return;
345      this.startLoader();
346
347      let dateTimeForm = {};
348      let isNTPEnabled = this.form.configurationSelected === 'ntp';
349
350      if (!isNTPEnabled) {
351        const isUtcDisplay = this.$store.getters['global/isUtcDisplay'];
352        let date;
353
354        dateTimeForm.ntpProtocolEnabled = false;
355
356        if (isUtcDisplay) {
357          // Create UTC Date
358          date = this.getUtcDate(this.form.manual.date, this.form.manual.time);
359        } else {
360          // Create local Date
361          date = new Date(`${this.form.manual.date} ${this.form.manual.time}`);
362        }
363
364        dateTimeForm.updatedDateTime = date.toISOString();
365      } else {
366        dateTimeForm.ntpProtocolEnabled = true;
367
368        const ntpArray = [
369          this.form.ntp.firstAddress,
370          this.form.ntp.secondAddress,
371          this.form.ntp.thirdAddress,
372        ];
373
374        // Filter the ntpArray to remove empty strings,
375        // per Redfish spec there should be no empty strings or null on the ntp array.
376        const ntpArrayFiltered = ntpArray.filter((x) => x);
377
378        dateTimeForm.ntpServersArray = [...ntpArrayFiltered];
379
380        [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]] = [
381          ...dateTimeForm.ntpServersArray,
382        ];
383
384        this.setNtpValues();
385      }
386
387      this.$store
388        .dispatch('dateTime/updateDateTime', dateTimeForm)
389        .then((success) => {
390          this.successToast(success);
391          if (!isNTPEnabled) return;
392          // Shift address up if second address is empty
393          // to avoid refreshing after delay when updating NTP
394          if (!this.form.ntp.secondAddress && this.form.ntp.thirdAddres) {
395            this.form.ntp.secondAddress = this.form.ntp.thirdAddres;
396            this.form.ntp.thirdAddress = '';
397          }
398        })
399        .then(() => {
400          this.$store.dispatch('global/getBmcTime');
401        })
402        .catch(({ message }) => this.errorToast(message))
403        .finally(() => {
404          this.v$.form.$reset();
405          this.endLoader();
406        });
407    },
408    getUtcDate(date, time) {
409      // Split user input string values to create
410      // a UTC Date object
411      const datesArray = date.split('-');
412      const timeArray = time.split(':');
413      let utcDate = Date.UTC(
414        datesArray[0], // User input year
415        //UTC expects zero-index month value 0-11 (January-December)
416        //for reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC#Parameters
417        parseInt(datesArray[1]) - 1, // User input month
418        datesArray[2], // User input day
419        timeArray[0], // User input hour
420        timeArray[1], // User input minute
421      );
422      return new Date(utcDate);
423    },
424  },
425};
426</script>
427