xref: /openbmc/webui-vue/src/views/Settings/DateTime/DateTime.vue (revision 7d6b44cb263da09e575c7cb28cab88c1eb339c7b)
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">{{ 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">{{ 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">
67                      {{ $t('global.form.invalidFormat') }}
68                    </div>
69                    <div v-if="!$v.form.manual.date.required">
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">
115                      {{ $t('global.form.invalidFormat') }}
116                    </div>
117                    <div v-if="!$v.form.manual.time.required">
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">
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, helpers } from '@vuelidate/validators';
213
214const isoDateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/;
215const isoTimeRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/;
216
217export default {
218  name: 'DateTime',
219  components: { Alert, IconCalendar, PageTitle, PageSection },
220  mixins: [
221    BVToastMixin,
222    LoadingBarMixin,
223    LocalTimezoneLabelMixin,
224    VuelidateMixin,
225  ],
226  beforeRouteLeave(to, from, next) {
227    this.hideLoader();
228    next();
229  },
230  setup() {
231    return {
232      v$: useVuelidate(),
233    };
234  },
235  data() {
236    return {
237      locale: this.$store.getters['global/languagePreference'],
238      form: {
239        configurationSelected: 'manual',
240        manual: {
241          date: '',
242          time: '',
243        },
244        ntp: { firstAddress: '', secondAddress: '', thirdAddress: '' },
245      },
246      loading,
247    };
248  },
249  validations() {
250    return {
251      form: {
252        manual: {
253          date: {
254            required: requiredIf(function () {
255              return this.form.configurationSelected === 'manual';
256            }),
257            pattern: helpers.regex('pattern', isoDateRegex),
258          },
259          time: {
260            required: requiredIf(function () {
261              return this.form.configurationSelected === 'manual';
262            }),
263            pattern: helpers.regex('pattern', isoTimeRegex),
264          },
265        },
266        ntp: {
267          firstAddress: {
268            required: requiredIf(function () {
269              return this.form.configurationSelected === 'ntp';
270            }),
271          },
272        },
273      },
274    };
275  },
276  computed: {
277    ...mapState('dateTime', ['ntpServers', 'isNtpProtocolEnabled']),
278    bmcTime() {
279      return this.$store.getters['global/bmcTime'];
280    },
281    ntpOptionSelected() {
282      return this.form.configurationSelected === 'ntp';
283    },
284    manualOptionSelected() {
285      return this.form.configurationSelected === 'manual';
286    },
287    isUtcDisplay() {
288      return this.$store.getters['global/isUtcDisplay'];
289    },
290    timezone() {
291      if (this.isUtcDisplay) {
292        return 'UTC';
293      }
294      return this.localOffset();
295    },
296  },
297  watch: {
298    ntpServers() {
299      this.setNtpValues();
300    },
301    manualDate() {
302      this.emitChange();
303    },
304    bmcTime() {
305      this.form.manual.date = this.$options.filters.formatDate(
306        this.$store.getters['global/bmcTime'],
307      );
308      this.form.manual.time = this.$options.filters
309        .formatTime(this.$store.getters['global/bmcTime'])
310        .slice(0, 5);
311    },
312  },
313  created() {
314    this.startLoader();
315    this.setNtpValues();
316    Promise.all([
317      this.$store.dispatch('global/getBmcTime'),
318      this.$store.dispatch('dateTime/getNtpData'),
319    ]).finally(() => this.endLoader());
320  },
321  methods: {
322    emitChange() {
323      if (this.$v.$invalid) return;
324      this.$v.$reset(); //reset to re-validate on blur
325      this.$emit('change', {
326        manualDate: this.manualDate ? new Date(this.manualDate) : null,
327      });
328    },
329    setNtpValues() {
330      this.form.configurationSelected = this.isNtpProtocolEnabled
331        ? 'ntp'
332        : 'manual';
333      [
334        this.form.ntp.firstAddress = '',
335        this.form.ntp.secondAddress = '',
336        this.form.ntp.thirdAddress = '',
337      ] = [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]];
338    },
339    submitForm() {
340      this.$v.$touch();
341      if (this.$v.$invalid) return;
342      this.startLoader();
343
344      let dateTimeForm = {};
345      let isNTPEnabled = this.form.configurationSelected === 'ntp';
346
347      if (!isNTPEnabled) {
348        const isUtcDisplay = this.$store.getters['global/isUtcDisplay'];
349        let date;
350
351        dateTimeForm.ntpProtocolEnabled = false;
352
353        if (isUtcDisplay) {
354          // Create UTC Date
355          date = this.getUtcDate(this.form.manual.date, this.form.manual.time);
356        } else {
357          // Create local Date
358          date = new Date(`${this.form.manual.date} ${this.form.manual.time}`);
359        }
360
361        dateTimeForm.updatedDateTime = date.toISOString();
362      } else {
363        dateTimeForm.ntpProtocolEnabled = true;
364
365        const ntpArray = [
366          this.form.ntp.firstAddress,
367          this.form.ntp.secondAddress,
368          this.form.ntp.thirdAddress,
369        ];
370
371        // Filter the ntpArray to remove empty strings,
372        // per Redfish spec there should be no empty strings or null on the ntp array.
373        const ntpArrayFiltered = ntpArray.filter((x) => x);
374
375        dateTimeForm.ntpServersArray = [...ntpArrayFiltered];
376
377        [this.ntpServers[0], this.ntpServers[1], this.ntpServers[2]] = [
378          ...dateTimeForm.ntpServersArray,
379        ];
380
381        this.setNtpValues();
382      }
383
384      this.$store
385        .dispatch('dateTime/updateDateTime', dateTimeForm)
386        .then((success) => {
387          this.successToast(success);
388          if (!isNTPEnabled) return;
389          // Shift address up if second address is empty
390          // to avoid refreshing after delay when updating NTP
391          if (!this.form.ntp.secondAddress && this.form.ntp.thirdAddres) {
392            this.form.ntp.secondAddress = this.form.ntp.thirdAddres;
393            this.form.ntp.thirdAddress = '';
394          }
395        })
396        .then(() => {
397          this.$store.dispatch('global/getBmcTime');
398        })
399        .catch(({ message }) => this.errorToast(message))
400        .finally(() => {
401          this.$v.form.$reset();
402          this.endLoader();
403        });
404    },
405    getUtcDate(date, time) {
406      // Split user input string values to create
407      // a UTC Date object
408      const datesArray = date.split('-');
409      const timeArray = time.split(':');
410      let utcDate = Date.UTC(
411        datesArray[0], // User input year
412        //UTC expects zero-index month value 0-11 (January-December)
413        //for reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC#Parameters
414        parseInt(datesArray[1]) - 1, // User input month
415        datesArray[2], // User input day
416        timeArray[0], // User input hour
417        timeArray[1], // User input minute
418      );
419      return new Date(utcDate);
420    },
421  },
422};
423</script>
424