Commit 4f08d9d5 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '197879-add-relative-time-ranges-to-datepicker-in-component' into 'master'

Use time ranges in date picker (component)

See merge request gitlab-org/gitlab!24068
parents 6f359a67 488ba880
...@@ -19,10 +19,10 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; ...@@ -19,10 +19,10 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { getTimeRange } from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
...@@ -31,11 +31,8 @@ import GroupEmptyState from './group_empty_state.vue'; ...@@ -31,11 +31,8 @@ import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue'; import DashboardsDropdown from './dashboards_dropdown.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions } from '../utils'; import { getAddMetricTrackingOptions, timeRangeToUrl, timeRangeFromUrl } from '../utils';
import { defaultTimeRange, timeRanges, metricStates } from '../constants';
import { datePickerTimeWindows, metricStates } from '../constants';
const defaultTimeRange = getTimeRange();
export default { export default {
components: { components: {
...@@ -197,10 +194,9 @@ export default { ...@@ -197,10 +194,9 @@ export default {
return { return {
state: 'gettingStarted', state: 'gettingStarted',
formIsValid: null, formIsValid: null,
startDate: getParameterValues('start')[0] || defaultTimeRange.start, selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
endDate: getParameterValues('end')[0] || defaultTimeRange.end,
hasValidDates: true, hasValidDates: true,
datePickerTimeWindows, timeRanges,
isRearrangingPanels: false, isRearrangingPanels: false,
}; };
}, },
...@@ -260,9 +256,11 @@ export default { ...@@ -260,9 +256,11 @@ export default {
if (!this.hasMetrics) { if (!this.hasMetrics) {
this.setGettingStartedEmptyState(); this.setGettingStartedEmptyState();
} else { } else {
const { start, end } = convertToFixedRange(this.selectedTimeRange);
this.fetchData({ this.fetchData({
start: this.startDate, start,
end: this.endDate, end,
}); });
} }
}, },
...@@ -287,8 +285,8 @@ export default { ...@@ -287,8 +285,8 @@ export default {
}); });
}, },
onDateTimePickerApply(params) { onDateTimePickerInput(timeRange) {
redirectTo(mergeUrlParams(params, window.location.href)); redirectTo(timeRangeToUrl(timeRange));
}, },
onDateTimePickerInvalid() { onDateTimePickerInvalid() {
createFlash( createFlash(
...@@ -296,8 +294,8 @@ export default { ...@@ -296,8 +294,8 @@ export default {
'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.', 'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.',
), ),
); );
this.startDate = defaultTimeRange.start; // As a fallback, switch to default time range instead
this.endDate = defaultTimeRange.end; this.selectedTimeRange = defaultTimeRange;
}, },
generateLink(group, title, yLabel) { generateLink(group, title, yLabel) {
...@@ -447,10 +445,9 @@ export default { ...@@ -447,10 +445,9 @@ export default {
> >
<date-time-picker <date-time-picker
ref="dateTimePicker" ref="dateTimePicker"
:start="startDate" :value="selectedTimeRange"
:end="endDate" :options="timeRanges"
:time-windows="datePickerTimeWindows" @input="onDateTimePickerInput"
@apply="onDateTimePickerApply"
@invalid="onDateTimePickerInvalid" @invalid="onDateTimePickerInvalid"
/> />
</gl-form-group> </gl-form-group>
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { sidebarAnimationDuration } from '../constants'; import { timeRangeFromUrl, removeTimeRangeParams } from '../utils';
import { getTimeRange } from '~/vue_shared/components/date_time_picker/date_time_picker_lib'; import { sidebarAnimationDuration, defaultTimeRange } from '../constants';
let sidebarMutationObserver; let sidebarMutationObserver;
...@@ -18,10 +18,8 @@ export default { ...@@ -18,10 +18,8 @@ export default {
}, },
}, },
data() { data() {
const defaultRange = getTimeRange(); const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
const start = getParameterValues('start', this.dashboardUrl)[0] || defaultRange.start; const { start, end } = convertToFixedRange(timeRange);
const end = getParameterValues('end', this.dashboardUrl)[0] || defaultRange.end;
const params = { const params = {
start, start,
end, end,
...@@ -81,7 +79,7 @@ export default { ...@@ -81,7 +79,7 @@ export default {
}, },
setInitialState() { setInitialState() {
this.setEndpoints({ this.setEndpoints({
dashboardEndpoint: removeParams(['start', 'end'], this.dashboardUrl), dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl),
}); });
this.setShowErrorBanner(false); this.setShowErrorBanner(false);
}, },
......
...@@ -83,34 +83,36 @@ export const dateFormats = { ...@@ -83,34 +83,36 @@ export const dateFormats = {
default: 'dd mmm yyyy, h:MMTT', default: 'dd mmm yyyy, h:MMTT',
}; };
export const datePickerTimeWindows = { export const timeRanges = [
thirtyMinutes: { {
label: __('30 minutes'), label: __('30 minutes'),
seconds: 60 * 30, duration: { seconds: 60 * 30 },
}, },
threeHours: { {
label: __('3 hours'), label: __('3 hours'),
seconds: 60 * 60 * 3, duration: { seconds: 60 * 60 * 3 },
}, },
eightHours: { {
label: __('8 hours'), label: __('8 hours'),
seconds: 60 * 60 * 8, duration: { seconds: 60 * 60 * 8 },
default: true, default: true,
}, },
oneDay: { {
label: __('1 day'), label: __('1 day'),
seconds: 60 * 60 * 24 * 1, duration: { seconds: 60 * 60 * 24 * 1 },
}, },
threeDays: { {
label: __('3 days'), label: __('3 days'),
seconds: 60 * 60 * 24 * 3, duration: { seconds: 60 * 60 * 24 * 3 },
}, },
oneWeek: { {
label: __('1 week'), label: __('1 week'),
seconds: 60 * 60 * 24 * 7 * 1, duration: { seconds: 60 * 60 * 24 * 7 * 1 },
}, },
twoWeeks: { {
label: __('2 weeks'), label: __('2 weeks'),
seconds: 60 * 60 * 24 * 7 * 2, duration: { seconds: 60 * 60 * 24 * 7 * 2 },
}, },
}; ];
export const defaultTimeRange = timeRanges.find(tr => tr.default);
<script> <script>
import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DateTimePickerInput from './date_time_picker_input.vue'; import DateTimePickerInput from './date_time_picker_input.vue';
import { import {
defaultTimeWindows, defaultTimeRanges,
defaultTimeRange,
isValidDate, isValidDate,
getTimeRange,
getTimeWindowKey,
stringToISODate, stringToISODate,
ISODateToString, ISODateToString,
truncateZerosInDateTime, truncateZerosInDateTime,
...@@ -15,7 +17,7 @@ import { ...@@ -15,7 +17,7 @@ import {
} from './date_time_picker_lib'; } from './date_time_picker_lib';
const events = { const events = {
apply: 'apply', input: 'input',
invalid: 'invalid', invalid: 'invalid',
}; };
...@@ -29,24 +31,22 @@ export default { ...@@ -29,24 +31,22 @@ export default {
GlDropdownItem, GlDropdownItem,
}, },
props: { props: {
start: { value: {
type: String,
required: true,
},
end: {
type: String,
required: true,
},
timeWindows: {
type: Object, type: Object,
required: false, required: false,
default: () => defaultTimeWindows, default: () => defaultTimeRange,
},
options: {
type: Array,
required: false,
default: () => defaultTimeRanges,
}, },
}, },
data() { data() {
return { return {
startDate: this.start, timeRange: this.value,
endDate: this.end, startDate: '',
endDate: '',
}; };
}, },
computed: { computed: {
...@@ -67,6 +67,7 @@ export default { ...@@ -67,6 +67,7 @@ export default {
set(val) { set(val) {
// Attempt to set a formatted date if possible // Attempt to set a formatted date if possible
this.startDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val; this.startDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val;
this.timeRange = null;
}, },
}, },
endInput: { endInput: {
...@@ -76,23 +77,48 @@ export default { ...@@ -76,23 +77,48 @@ export default {
set(val) { set(val) {
// Attempt to set a formatted date if possible // Attempt to set a formatted date if possible
this.endDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val; this.endDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val;
this.timeRange = null;
}, },
}, },
timeWindowText() { timeWindowText() {
const timeWindow = getTimeWindowKey({ start: this.start, end: this.end }, this.timeWindows); try {
if (timeWindow) { const timeRange = findTimeRange(this.value, this.options);
return this.timeWindows[timeWindow].label; if (timeRange) {
} else if (isValidDate(this.start) && isValidDate(this.end)) { return timeRange.label;
}
const { start, end } = convertToFixedRange(this.value);
if (isValidDate(start) && isValidDate(end)) {
return sprintf(__('%{start} to %{end}'), { return sprintf(__('%{start} to %{end}'), {
start: this.formatDate(this.start), start: this.formatDate(start),
end: this.formatDate(this.end), end: this.formatDate(end),
}); });
} }
} catch {
return __('Invalid date range');
}
return ''; return '';
}, },
}, },
watch: {
value(newValue) {
const { start, end } = convertToFixedRange(newValue);
this.timeRange = this.value;
this.startDate = start;
this.endDate = end;
},
},
mounted() { mounted() {
try {
const { start, end } = convertToFixedRange(this.timeRange);
this.startDate = start;
this.endDate = end;
} catch {
// when dates cannot be parsed, emit error.
this.$emit(events.invalid);
}
// Validate on mounted, and trigger an update if needed // Validate on mounted, and trigger an update if needed
if (!this.isValid) { if (!this.isValid) {
this.$emit(events.invalid); this.$emit(events.invalid);
...@@ -102,21 +128,22 @@ export default { ...@@ -102,21 +128,22 @@ export default {
formatDate(date) { formatDate(date) {
return truncateZerosInDateTime(ISODateToString(date)); return truncateZerosInDateTime(ISODateToString(date));
}, },
setTimeWindow(key) {
const { start, end } = getTimeRange(key, this.timeWindows);
this.startDate = start;
this.endDate = end;
this.apply();
},
closeDropdown() { closeDropdown() {
this.$refs.dropdown.hide(); this.$refs.dropdown.hide();
}, },
apply() { isOptionActive(option) {
this.$emit(events.apply, { return isEqualTimeRanges(option, this.timeRange);
},
setQuickRange(option) {
this.timeRange = option;
this.$emit(events.input, this.timeRange);
},
setFixedRange() {
this.timeRange = convertToFixedRange({
start: this.startDate, start: this.startDate,
end: this.endDate, end: this.endDate,
}); });
this.$emit(events.input, this.timeRange);
}, },
}, },
}; };
...@@ -146,7 +173,7 @@ export default { ...@@ -146,7 +173,7 @@ export default {
</div> </div>
<gl-form-group> <gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button variant="success" :disabled="!isValid" @click="apply()"> <gl-button variant="success" :disabled="!isValid" @click="setFixedRange()">
{{ __('Apply') }} {{ __('Apply') }}
</gl-button> </gl-button>
</gl-form-group> </gl-form-group>
...@@ -155,19 +182,20 @@ export default { ...@@ -155,19 +182,20 @@ export default {
<template #label> <template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span> <span class="gl-pl-5">{{ __('Quick range') }}</span>
</template> </template>
<gl-dropdown-item <gl-dropdown-item
v-for="(timeWindow, key) in timeWindows" v-for="(option, index) in options"
:key="key" :key="index"
:active="timeWindow.label === timeWindowText" :active="isOptionActive(option)"
active-class="active" active-class="active"
@click="setTimeWindow(key)" @click="setQuickRange(option)"
> >
<icon <icon
name="mobile-issue-close" name="mobile-issue-close"
class="align-bottom" class="align-bottom"
:class="{ invisible: timeWindow.label !== timeWindowText }" :class="{ invisible: !isOptionActive(option) }"
/> />
{{ timeWindow.label }} {{ option.label }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-form-group> </gl-form-group>
</div> </div>
......
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
/** /**
* Valid strings for this regex are * Valid strings for this regex are
...@@ -9,37 +8,30 @@ import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; ...@@ -9,37 +8,30 @@ import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
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]))?$/; 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". * Default time ranges for the date picker.
* * @see app/assets/javascripts/lib/utils/datetime_range.js
* 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 = { export const defaultTimeRanges = [
thirtyMinutes: { {
duration: { seconds: 60 * 30 },
label: __('30 minutes'), label: __('30 minutes'),
seconds: 60 * 30,
}, },
threeHours: { {
duration: { seconds: 60 * 60 * 3 },
label: __('3 hours'), label: __('3 hours'),
seconds: 60 * 60 * 3,
}, },
eightHours: { {
duration: { seconds: 60 * 60 * 8 },
label: __('8 hours'), label: __('8 hours'),
seconds: 60 * 60 * 8,
default: true, default: true,
}, },
oneDay: { {
duration: { seconds: 60 * 60 * 24 * 1 },
label: __('1 day'), label: __('1 day'),
seconds: 60 * 60 * 24 * 1,
}, },
threeDays: { ];
label: __('3 days'),
seconds: 60 * 60 * 24 * 3, export const defaultTimeRange = defaultTimeRanges.find(tr => tr.default);
},
};
export const dateFormats = { export const dateFormats = {
ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'", ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'",
...@@ -67,46 +59,6 @@ export const isValidDate = dateString => { ...@@ -67,46 +59,6 @@ export const isValidDate = dateString => {
} }
}; };
/**
* 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. * Convert the input in Time picker component to ISO date.
* *
......
---
title: Allow for relative time ranges in metrics dashboard URLs
merge_request: 23765
author:
type: added
...@@ -10431,6 +10431,9 @@ msgstr "" ...@@ -10431,6 +10431,9 @@ msgstr ""
msgid "Invalid date format. Please use UTC format as YYYY-MM-DD" msgid "Invalid date format. Please use UTC format as YYYY-MM-DD"
msgstr "" msgstr ""
msgid "Invalid date range"
msgstr ""
msgid "Invalid feature" msgid "Invalid feature"
msgstr "" msgstr ""
......
...@@ -69,9 +69,8 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -69,9 +69,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
label-size="sm" label-size="sm"
> >
<date-time-picker-stub <date-time-picker-stub
end="2020-01-01T18:57:47.000Z" options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
start="2020-01-01T18:27:47.000Z" value="[object Object]"
timewindows="[object Object]"
/> />
</gl-form-group-stub> </gl-form-group-stub>
......
...@@ -5,13 +5,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue'; ...@@ -5,13 +5,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils'; import { propsData } from '../init_utils';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility');
getParameterValues: jest.fn().mockImplementation(param => {
if (param === 'start') return ['2020-01-01T18:27:47.000Z'];
if (param === 'end') return ['2020-01-01T18:57:47.000Z'];
return [];
}),
}));
describe('Dashboard template', () => { describe('Dashboard template', () => {
let wrapper; let wrapper;
......
import { mount } from '@vue/test-utils';
import createFlash from '~/flash';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockReturnValue('<script>alert("XSS")</script>'),
}));
describe('dashboard invalid url parameters', () => {
let store;
let wrapper;
let mock;
const createMountedWrapper = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
it('shows an error message if invalid url parameters are passed', done => {
createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
wrapper.vm
.$nextTick()
.then(() => {
expect(createFlash).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
});
import { mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData, setupComponentStore } from '../init_utils';
import { metricsDashboardPayload, mockApiEndpoint } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterValues: jest.fn().mockImplementation(param => {
if (param === 'start') return ['2019-10-01T18:27:47.000Z'];
if (param === 'end') return ['2019-10-01T18:57:47.000Z'];
return [];
}),
mergeUrlParams: jest.fn().mockReturnValue('#'),
}));
describe('dashboard time window', () => {
let store;
let wrapper;
let mock;
const createComponentWrapperMounted = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
});
it('shows an active quick range option', done => {
mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsDashboardPayload);
createComponentWrapperMounted({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
setupComponentStore(wrapper);
wrapper.vm
.$nextTick()
.then(() => {
const timeWindowDropdownItems = wrapper
.find({ ref: 'dateTimePicker' })
.findAll(GlDropdownItem);
const activeItem = timeWindowDropdownItems.wrappers.filter(itemWrapper =>
itemWrapper.find('.active').exists(),
);
expect(activeItem.length).toBe(1);
done();
})
.catch(done.fail);
});
});
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import { queryToObject, redirectTo, removeParams, mergeUrlParams } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { mockProjectDir } from '../mock_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('dashboard invalid url parameters', () => {
let store;
let wrapper;
let mock;
const fetchDataMock = jest.fn();
const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
stubs: ['graph-group', 'panel-type'],
methods: {
fetchData: fetchDataMock,
},
...options,
});
};
const findDateTimePicker = () => wrapper.find({ ref: 'dateTimePicker' });
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
mock.restore();
fetchDataMock.mockReset();
queryToObject.mockReset();
});
it('passes default url parameters to the time range picker', () => {
queryToObject.mockReturnValue({});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('passes a fixed time range in the URL to the time range picker', () => {
const params = {
start: '2019-01-01T00:00:00.000Z',
end: '2019-01-10T00:00:00.000Z',
};
queryToObject.mockReturnValue(params);
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toEqual(params);
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith(params);
});
});
it('passes a rolling time range in the URL to the time range picker', () => {
queryToObject.mockReturnValue({
duration_seconds: '120',
});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 60 * 2 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('shows an error message and loads a default time range if invalid url parameters are passed', () => {
queryToObject.mockReturnValue({
start: '<script>alert("XSS")</script>',
end: '<script>alert("XSS")</script>',
});
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalled();
expect(findDateTimePicker().props('value')).toMatchObject({
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String),
end: expect.any(String),
});
});
});
it('redirects to different time range', () => {
const toUrl = `${mockProjectDir}/-/environments/1/metrics`;
removeParams.mockReturnValueOnce(toUrl);
createMountedWrapper();
return wrapper.vm.$nextTick().then(() => {
findDateTimePicker().vm.$emit('input', {
duration: { seconds: 120 },
});
// redirect to plus + new parameters
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
expect(redirectTo).toHaveBeenCalledTimes(1);
});
});
});
...@@ -54,97 +54,6 @@ describe('date time picker lib', () => { ...@@ -54,97 +54,6 @@ describe('date time picker lib', () => {
}); });
}); });
describe('getTimeWindow', () => {
[
{
args: [
{
start: '2019-10-01T18:27:47.000Z',
end: '2019-10-01T21:27:47.000Z',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: 'threeHours',
},
{
args: [
{
start: '2019-10-01T28:27:47.000Z',
end: '2019-10-01T21:27:47.000Z',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [
{
start: '',
end: '',
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [
{
start: null,
end: null,
},
dateTimePickerLib.defaultTimeWindows,
],
expected: null,
},
{
args: [{}, dateTimePickerLib.defaultTimeWindows],
expected: null,
},
].forEach(({ args, expected }) => {
it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
expect(dateTimePickerLib.getTimeWindowKey(...args)).toEqual(expected);
});
});
});
describe('getTimeRange', () => {
function secondsBetween({ start, end }) {
return (new Date(end) - new Date(start)) / 1000;
}
function minutesBetween(timeRange) {
return secondsBetween(timeRange) / 60;
}
function hoursBetween(timeRange) {
return minutesBetween(timeRange) / 60;
}
it('defaults to an 8 hour (28800s) difference', () => {
const params = dateTimePickerLib.getTimeRange();
expect(hoursBetween(params)).toEqual(8);
});
it('accepts time window as an argument', () => {
const params = dateTimePickerLib.getTimeRange('thirtyMinutes');
expect(minutesBetween(params)).toEqual(30);
});
it('returns a value for every defined time window', () => {
const nonDefaultWindows = Object.entries(dateTimePickerLib.defaultTimeWindows).filter(
([, timeWindow]) => !timeWindow.default,
);
nonDefaultWindows.forEach(timeWindow => {
const params = dateTimePickerLib.getTimeRange(timeWindow[0]);
// Ensure we're not returning the default
expect(hoursBetween(params)).not.toEqual(8);
});
});
});
describe('stringToISODate', () => { describe('stringToISODate', () => {
['', 'null', undefined, 'abc'].forEach(input => { ['', 'null', undefined, 'abc'].forEach(input => {
it(`throws error for invalid input like ${input}`, done => { it(`throws error for invalid input like ${input}`, done => {
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { defaultTimeWindows } from '~/vue_shared/components/date_time_picker/date_time_picker_lib'; import {
defaultTimeRanges,
defaultTimeRange,
} from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
const timeWindowsCount = Object.entries(defaultTimeWindows).length; const optionsCount = defaultTimeRanges.length;
const start = '2019-10-10T07:00:00.000Z';
const end = '2019-10-13T07:00:00.000Z';
const selectedTimeWindowText = `3 days`;
describe('DateTimePicker', () => { describe('DateTimePicker', () => {
let dateTimePicker; let dateTimePicker;
...@@ -15,19 +15,10 @@ describe('DateTimePicker', () => { ...@@ -15,19 +15,10 @@ describe('DateTimePicker', () => {
const applyButtonElement = () => dateTimePicker.find('button.btn-success').element; const applyButtonElement = () => dateTimePicker.find('button.btn-success').element;
const findQuickRangeItems = () => dateTimePicker.findAll('.dropdown-item'); const findQuickRangeItems = () => dateTimePicker.findAll('.dropdown-item');
const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element; const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
const fillInputAndBlur = (input, val) => {
dateTimePicker.find(input).setValue(val);
return dateTimePicker.vm.$nextTick().then(() => {
dateTimePicker.find(input).trigger('blur');
return dateTimePicker.vm.$nextTick();
});
};
const createComponent = props => { const createComponent = props => {
dateTimePicker = mount(DateTimePicker, { dateTimePicker = mount(DateTimePicker, {
propsData: { propsData: {
start,
end,
...props, ...props,
}, },
}); });
...@@ -40,7 +31,7 @@ describe('DateTimePicker', () => { ...@@ -40,7 +31,7 @@ describe('DateTimePicker', () => {
it('renders dropdown toggle button with selected text', done => { it('renders dropdown toggle button with selected text', done => {
createComponent(); createComponent();
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe(selectedTimeWindowText); expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
done(); done();
}); });
}); });
...@@ -54,8 +45,10 @@ describe('DateTimePicker', () => { ...@@ -54,8 +45,10 @@ describe('DateTimePicker', () => {
it('renders inputs with h/m/s truncated if its all 0s', done => { it('renders inputs with h/m/s truncated if its all 0s', done => {
createComponent({ createComponent({
value: {
start: '2019-10-10T00:00:00.000Z', start: '2019-10-10T00:00:00.000Z',
end: '2019-10-14T00:10:00.000Z', end: '2019-10-14T00:10:00.000Z',
},
}); });
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10'); expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
...@@ -64,22 +57,21 @@ describe('DateTimePicker', () => { ...@@ -64,22 +57,21 @@ describe('DateTimePicker', () => {
}); });
}); });
it(`renders dropdown with ${timeWindowsCount} (default) items in quick range`, done => { it(`renders dropdown with ${optionsCount} (default) items in quick range`, done => {
createComponent(); createComponent();
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(findQuickRangeItems().length).toBe(timeWindowsCount); expect(findQuickRangeItems().length).toBe(optionsCount);
done(); done();
}); });
}); });
it(`renders dropdown with correct quick range item selected`, done => { it('renders dropdown with a default quick range item selected', done => {
createComponent(); createComponent();
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(selectedTimeWindowText); expect(dateTimePicker.find('.dropdown-item.active').exists()).toBe(true);
expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
expect(dateTimePicker.find('.dropdown-item.active svg').isVisible()).toBe(true);
done(); done();
}); });
}); });
...@@ -92,8 +84,21 @@ describe('DateTimePicker', () => { ...@@ -92,8 +84,21 @@ describe('DateTimePicker', () => {
expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
}); });
it('displays inline error message if custom time range inputs are invalid', done => { describe('user input', () => {
const fillInputAndBlur = (input, val) => {
dateTimePicker.find(input).setValue(val);
return dateTimePicker.vm.$nextTick().then(() => {
dateTimePicker.find(input).trigger('blur');
return dateTimePicker.vm.$nextTick();
});
};
beforeEach(done => {
createComponent(); createComponent();
dateTimePicker.vm.$nextTick(done);
});
it('displays inline error message if custom time range inputs are invalid', done => {
fillInputAndBlur('#custom-time-from', '2019-10-01abc') fillInputAndBlur('#custom-time-from', '2019-10-01abc')
.then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc')) .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc'))
.then(() => { .then(() => {
...@@ -104,7 +109,6 @@ describe('DateTimePicker', () => { ...@@ -104,7 +109,6 @@ describe('DateTimePicker', () => {
}); });
it('keeps apply button disabled with invalid custom time range inputs', done => { it('keeps apply button disabled with invalid custom time range inputs', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01abc') fillInputAndBlur('#custom-time-from', '2019-10-01abc')
.then(() => fillInputAndBlur('#custom-time-to', '2019-09-19')) .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19'))
.then(() => { .then(() => {
...@@ -115,7 +119,6 @@ describe('DateTimePicker', () => { ...@@ -115,7 +119,6 @@ describe('DateTimePicker', () => {
}); });
it('enables apply button with valid custom time range inputs', done => { it('enables apply button with valid custom time range inputs', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01') fillInputAndBlur('#custom-time-from', '2019-10-01')
.then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
.then(() => { .then(() => {
...@@ -126,14 +129,13 @@ describe('DateTimePicker', () => { ...@@ -126,14 +129,13 @@ describe('DateTimePicker', () => {
}); });
it('emits dates in an object when apply is clicked', done => { it('emits dates in an object when apply is clicked', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01') fillInputAndBlur('#custom-time-from', '2019-10-01')
.then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
.then(() => { .then(() => {
applyButtonElement().click(); applyButtonElement().click();
expect(dateTimePicker.emitted().apply).toHaveLength(1); expect(dateTimePicker.emitted().input).toHaveLength(1);
expect(dateTimePicker.emitted().apply[0]).toEqual([ expect(dateTimePicker.emitted().input[0]).toEqual([
{ {
end: '2019-10-19T00:00:00Z', end: '2019-10-19T00:00:00Z',
start: '2019-10-01T00:00:00Z', start: '2019-10-01T00:00:00Z',
...@@ -144,8 +146,34 @@ describe('DateTimePicker', () => { ...@@ -144,8 +146,34 @@ describe('DateTimePicker', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('unchecks quick range when text is input is clicked', done => {
const findActiveItems = () => findQuickRangeItems().filter(w => w.is('.active'));
expect(findActiveItems().length).toBe(1);
fillInputAndBlur('#custom-time-from', '2019-10-01')
.then(() => {
expect(findActiveItems().length).toBe(0);
done();
})
.catch(done.fail);
});
it('emits dates in an object when a is clicked', () => {
findQuickRangeItems()
.at(3) // any item
.trigger('click');
expect(dateTimePicker.emitted().input).toHaveLength(1);
expect(dateTimePicker.emitted().input[0][0]).toMatchObject({
duration: {
seconds: expect.any(Number),
},
});
});
it('hides the popover with cancel button', done => { it('hides the popover with cancel button', done => {
createComponent();
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
...@@ -157,34 +185,41 @@ describe('DateTimePicker', () => { ...@@ -157,34 +185,41 @@ describe('DateTimePicker', () => {
}); });
}); });
}); });
});
describe('when using non-default time windows', () => { describe('when using non-default time windows', () => {
const otherTimeWindows = { const MOCK_NOW = Date.UTC(2020, 0, 23, 20);
oneMinute: {
const otherTimeRanges = [
{
label: '1 minute', label: '1 minute',
seconds: 60, duration: { seconds: 60 },
}, },
twoMinutes: { {
label: '2 minutes', label: '2 minutes',
seconds: 60 * 2, duration: { seconds: 60 * 2 },
default: true, default: true,
}, },
fiveMinutes: { {
label: '5 minutes', label: '5 minutes',
seconds: 60 * 5, duration: { seconds: 60 * 5 },
}, },
}; ];
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
});
it('renders dropdown with a label in the quick range', done => { it('renders dropdown with a label in the quick range', done => {
createComponent({ createComponent({
// 2 minutes range value: {
start: '2020-01-21T15:00:00.000Z', duration: { seconds: 60 * 5 },
end: '2020-01-21T15:02:00.000Z', },
timeWindows: otherTimeWindows, options: otherTimeRanges,
}); });
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe('2 minutes'); expect(dropdownToggle().text()).toBe('5 minutes');
done(); done();
}); });
...@@ -192,16 +227,16 @@ describe('DateTimePicker', () => { ...@@ -192,16 +227,16 @@ describe('DateTimePicker', () => {
it('renders dropdown with quick range items', done => { it('renders dropdown with quick range items', done => {
createComponent({ createComponent({
// 2 minutes range value: {
start: '2020-01-21T15:00:00.000Z', duration: { seconds: 60 * 2 },
end: '2020-01-21T15:02:00.000Z', },
timeWindows: otherTimeWindows, options: otherTimeRanges,
}); });
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
const items = findQuickRangeItems(); const items = findQuickRangeItems();
expect(items.length).toBe(Object.keys(otherTimeWindows).length); expect(items.length).toBe(Object.keys(otherTimeRanges).length);
expect(items.at(0).text()).toBe('1 minute'); expect(items.at(0).text()).toBe('1 minute');
expect(items.at(0).is('.active')).toBe(false); expect(items.at(0).is('.active')).toBe(false);
...@@ -217,14 +252,13 @@ describe('DateTimePicker', () => { ...@@ -217,14 +252,13 @@ describe('DateTimePicker', () => {
it('renders dropdown with a label not in the quick range', done => { it('renders dropdown with a label not in the quick range', done => {
createComponent({ createComponent({
// 10 minutes range value: {
start: '2020-01-21T15:00:00.000Z', duration: { seconds: 60 * 4 },
end: '2020-01-21T15:10:00.000Z', },
timeWindows: otherTimeWindows,
}); });
dropdownToggle().trigger('click'); dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => { dateTimePicker.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe('2020-01-21 15:00:00 to 2020-01-21 15:10:00'); expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
done(); done();
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment