Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-24 12:08:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-24 12:08:32 +0300
commit9b984f55eef568b6a15c3a125e0cf66f35678e5a (patch)
treeee7e1eb42f27400dd74bb44bb595263af2d72fc1 /app/assets/javascripts/vue_shared/components/date_time_picker
parent83a9f472b8b523619519a1834176165c9f1532f7 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/date_time_picker')
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue175
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js132
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 {};