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