diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-24 12:08:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-24 12:08:32 +0300 |
commit | 9b984f55eef568b6a15c3a125e0cf66f35678e5a (patch) | |
tree | ee7e1eb42f27400dd74bb44bb595263af2d72fc1 /app/assets/javascripts/vue_shared/components/date_time_picker | |
parent | 83a9f472b8b523619519a1834176165c9f1532f7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/date_time_picker')
3 files changed, 384 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue new file mode 100644 index 00000000000..7d4c162473f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue @@ -0,0 +1,175 @@ +<script> +import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import DateTimePickerInput from './date_time_picker_input.vue'; +import { + defaultTimeWindows, + isValidDate, + getTimeRange, + getTimeWindowKey, + stringToISODate, + ISODateToString, + truncateZerosInDateTime, + isDateTimePickerInputValid, +} from './date_time_picker_lib'; + +const events = { + apply: 'apply', + invalid: 'invalid', +}; + +export default { + components: { + Icon, + DateTimePickerInput, + GlFormGroup, + GlButton, + GlDropdown, + GlDropdownItem, + }, + props: { + start: { + type: String, + required: true, + }, + end: { + type: String, + required: true, + }, + timeWindows: { + type: Object, + required: false, + default: () => defaultTimeWindows, + }, + }, + data() { + return { + startDate: this.start, + endDate: this.end, + }; + }, + computed: { + startInputValid() { + return isValidDate(this.startDate); + }, + endInputValid() { + return isValidDate(this.endDate); + }, + isValid() { + return this.startInputValid && this.endInputValid; + }, + + startInput: { + get() { + return this.startInputValid ? this.formatDate(this.startDate) : this.startDate; + }, + set(val) { + // Attempt to set a formatted date if possible + this.startDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val; + }, + }, + endInput: { + get() { + return this.endInputValid ? this.formatDate(this.endDate) : this.endDate; + }, + set(val) { + // Attempt to set a formatted date if possible + this.endDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val; + }, + }, + + timeWindowText() { + const timeWindow = getTimeWindowKey({ start: this.start, end: this.end }, this.timeWindows); + if (timeWindow) { + return this.timeWindows[timeWindow].label; + } else if (isValidDate(this.start) && isValidDate(this.end)) { + return sprintf(__('%{start} to %{end}'), { + start: this.formatDate(this.start), + end: this.formatDate(this.end), + }); + } + return ''; + }, + }, + mounted() { + // Validate on mounted, and trigger an update if needed + if (!this.isValid) { + this.$emit(events.invalid); + } + }, + methods: { + formatDate(date) { + return truncateZerosInDateTime(ISODateToString(date)); + }, + setTimeWindow(key) { + const { start, end } = getTimeRange(key, this.timeWindows); + this.startDate = start; + this.endDate = end; + + this.apply(); + }, + closeDropdown() { + this.$refs.dropdown.hide(); + }, + apply() { + this.$emit(events.apply, { + start: this.startDate, + end: this.endDate, + }); + }, + }, +}; +</script> +<template> + <gl-dropdown :text="timeWindowText" class="date-time-picker" menu-class="date-time-picker-menu"> + <div class="d-flex justify-content-between gl-p-2"> + <gl-form-group + :label="__('Custom range')" + label-for="custom-from-time" + label-class="gl-pb-1" + class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0" + > + <div class="gl-pt-2"> + <date-time-picker-input + id="custom-time-from" + v-model="startInput" + :label="__('From')" + :state="startInputValid" + /> + <date-time-picker-input + id="custom-time-to" + v-model="endInput" + :label="__('To')" + :state="endInputValid" + /> + </div> + <gl-form-group> + <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> + <gl-button variant="success" :disabled="!isValid" @click="apply()"> + {{ __('Apply') }} + </gl-button> + </gl-form-group> + </gl-form-group> + <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0"> + <template #label> + <span class="gl-pl-5">{{ __('Quick range') }}</span> + </template> + <gl-dropdown-item + v-for="(timeWindow, key) in timeWindows" + :key="key" + :active="timeWindow.label === timeWindowText" + active-class="active" + @click="setTimeWindow(key)" + > + <icon + name="mobile-issue-close" + class="align-bottom" + :class="{ invisible: timeWindow.label !== timeWindowText }" + /> + {{ timeWindow.label }} + </gl-dropdown-item> + </gl-form-group> + </div> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue new file mode 100644 index 00000000000..f19f8bd46b3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue @@ -0,0 +1,77 @@ +<script> +import { uniqueId } from 'lodash'; +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import { dateFormats } from './date_time_picker_lib'; + +const inputGroupText = { + invalidFeedback: sprintf(__('Format: %{dateFormat}'), { + dateFormat: dateFormats.stringDate, + }), + placeholder: dateFormats.stringDate, +}; + +export default { + components: { + GlFormGroup, + GlFormInput, + }, + props: { + state: { + default: null, + required: true, + validator: prop => typeof prop === 'boolean' || prop === null, + }, + value: { + default: null, + required: false, + validator: prop => typeof prop === 'string' || prop === null, + }, + label: { + type: String, + default: '', + required: true, + }, + id: { + type: String, + required: false, + default: () => uniqueId('dateTimePicker_'), + }, + }, + data() { + return { + inputGroupText, + }; + }, + computed: { + invalidFeedback() { + return this.state ? '' : this.inputGroupText.invalidFeedback; + }, + inputState() { + // When the state is valid we want to show no + // green outline. Hence passing null and not true. + if (this.state === true) { + return null; + } + return this.state; + }, + }, + methods: { + onInputBlur(e) { + this.$emit('input', e.target.value.trim() || null); + }, + }, +}; +</script> + +<template> + <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback"> + <gl-form-input + :id="id" + :value="value" + :state="inputState" + :placeholder="inputGroupText.placeholder" + @blur="onInputBlur" + /> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js new file mode 100644 index 00000000000..685115b92dd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js @@ -0,0 +1,132 @@ +import dateformat from 'dateformat'; +import { __ } from '~/locale'; +import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; + +/** + * Valid strings for this regex are + * 2019-10-01 and 2019-10-01 01:02:03 + */ +const dateTimePickerRegex = /^(\d{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])(?: (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))?$/; + +/** + * A key-value pair of "time windows". + * + * A time window is a representation of period of time that starts + * some time in past until now. Keys are only used for easy reference. + * + * It is represented as user friendly `label` and number of `seconds` + * to be substracted from now. + */ +export const defaultTimeWindows = { + thirtyMinutes: { + label: __('30 minutes'), + seconds: 60 * 30, + }, + threeHours: { + label: __('3 hours'), + seconds: 60 * 60 * 3, + }, + eightHours: { + label: __('8 hours'), + seconds: 60 * 60 * 8, + default: true, + }, + oneDay: { + label: __('1 day'), + seconds: 60 * 60 * 24 * 1, + }, + threeDays: { + label: __('3 days'), + seconds: 60 * 60 * 24 * 3, + }, +}; + +export const dateFormats = { + ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'", + stringDate: 'yyyy-mm-dd HH:MM:ss', +}; + +/** + * The URL params start and end need to be validated + * before passing them down to other components. + * + * @param {string} dateString + * @returns true if the string is a valid date, false otherwise + */ +export const isValidDate = dateString => { + try { + // dateformat throws error that can be caught. + // This is better than using `new Date()` + if (dateString && dateString.trim()) { + dateformat(dateString, 'isoDateTime'); + return true; + } + return false; + } catch (e) { + return false; + } +}; + +/** + * For a given time window key (e.g. `threeHours`) and key-value pair + * object of time windows. + * + * Returns a date time range with start and end. + * + * @param {String} timeWindowKey - A key in the object of time windows. + * @param {Object} timeWindows - A key-value pair of time windows, + * with a second duration and a label. + * @returns An object with time range, start and end dates, in ISO format. + */ +export const getTimeRange = (timeWindowKey, timeWindows = defaultTimeWindows) => { + let difference; + if (timeWindows[timeWindowKey]) { + difference = timeWindows[timeWindowKey].seconds; + } else { + const [defaultEntry] = Object.entries(timeWindows).filter( + ([, timeWindow]) => timeWindow.default, + ); + // find default time window + difference = defaultEntry[1].seconds; + } + + const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds + const start = end - difference; + + return { + start: new Date(secondsToMilliseconds(start)).toISOString(), + end: new Date(secondsToMilliseconds(end)).toISOString(), + }; +}; + +export const getTimeWindowKey = ({ start, end }, timeWindows = defaultTimeWindows) => + Object.entries(timeWindows).reduce((acc, [timeWindowKey, timeWindow]) => { + if (new Date(end) - new Date(start) === secondsToMilliseconds(timeWindow.seconds)) { + return timeWindowKey; + } + return acc; + }, null); + +/** + * Convert the input in Time picker component to ISO date. + * + * @param {string} val + * @returns {string} + */ +export const stringToISODate = val => + dateformat(new Date(val.replace(/-/g, '/')), dateFormats.ISODate, true); + +/** + * Convert the ISO date received from the URL to string + * for the Time picker component. + * + * @param {Date} date + * @returns {string} + */ +export const ISODateToString = date => dateformat(date, dateFormats.stringDate); + +export const truncateZerosInDateTime = datetime => datetime.replace(' 00:00:00', ''); + +export const isDateTimePickerInputValid = val => dateTimePickerRegex.test(val); + +export default {}; |