Commit 7b85c6d0 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch...

Merge branch '29513-continue-improvements-for-time-window-filtering-on-metrics-dashboard' into 'master'

Continue improvements for time window filtering on metrics dashboard

Closes #29513

See merge request gitlab-org/gitlab!17554
parents 1433fc2e 15a6dcb4
......@@ -12,23 +12,19 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
import { sidebarAnimationDuration } from '../constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import {
getTimeDiff,
getTimeWindow,
downloadCSVOptions,
generateLinkToChartOptions,
} from '../utils';
import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
let sidebarMutationObserver;
......@@ -46,6 +42,7 @@ export default {
GlDropdownItem,
GlFormGroup,
GlModal,
DateTimePicker,
},
directives: {
GlModal: GlModalDirective,
......@@ -171,10 +168,8 @@ export default {
return {
state: 'gettingStarted',
elWidth: 0,
selectedTimeWindow: '',
selectedTimeWindowKey: '',
formIsValid: null,
timeWindows: {},
selectedTimeWindow: {},
isRearrangingPanels: false,
};
},
......@@ -237,11 +232,13 @@ export default {
end,
};
this.timeWindows = timeWindows;
this.selectedTimeWindowKey = getTimeWindow(range);
this.selectedTimeWindow = this.timeWindows[this.selectedTimeWindowKey];
this.selectedTimeWindow = range;
if (!isValidDate(start) || !isValidDate(end)) {
this.showInvalidDateError();
} else {
this.fetchData(range);
}
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
......@@ -298,6 +295,9 @@ export default {
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
metrics.splice(graphIndex, 1);
},
showInvalidDateError() {
createFlash(s__('Metrics|Link contains an invalid time window.'));
},
generateLink(group, title, yLabel) {
const dashboard = this.currentDashboard || this.firstDashboard.path;
const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
......@@ -320,16 +320,12 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindow;
},
setTimeWindowParameter(key) {
const { start, end } = getTimeDiff(key);
return `?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
},
groupHasData(group) {
return this.chartsWithData(group.metrics).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
downloadCSVOptions,
generateLinkToChartOptions,
},
......@@ -342,14 +338,14 @@ export default {
<template>
<div class="prometheus-graphs">
<div class="gl-p-3 pb-0 border-bottom bg-gray-light">
<div class="prometheus-graphs-header gl-p-3 pb-0 border-bottom bg-gray-light">
<div class="row">
<template v-if="environmentsEndpoint">
<gl-form-group
:label="__('Dashboard')"
label-size="sm"
label-for="monitor-dashboards-dropdown"
class="col-sm-12 col-md-4 col-lg-2"
class="col-sm-12 col-md-6 col-lg-2"
>
<gl-dropdown
id="monitor-dashboards-dropdown"
......@@ -372,7 +368,7 @@ export default {
:label="s__('Metrics|Environment')"
label-size="sm"
label-for="monitor-environments-dropdown"
class="col-sm-6 col-md-4 col-lg-2"
class="col-sm-6 col-md-6 col-lg-2"
>
<gl-dropdown
id="monitor-environments-dropdown"
......@@ -397,30 +393,19 @@ export default {
:label="s__('Metrics|Show last')"
label-size="sm"
label-for="monitor-time-window-dropdown"
class="col-sm-6 col-md-4 col-lg-2"
>
<gl-dropdown
id="monitor-time-window-dropdown"
class="mb-0 d-flex js-time-window-dropdown"
toggle-class="dropdown-menu-toggle"
:text="selectedTimeWindow"
class="col-sm-6 col-md-6 col-lg-4"
>
<gl-dropdown-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
:href="setTimeWindowParameter(key)"
active-class="active"
>{{ value }}</gl-dropdown-item
>
</gl-dropdown>
<date-time-picker
:selected-time-window="selectedTimeWindow"
@onApply="onDateTimePickerApply"
/>
</gl-form-group>
</template>
<gl-form-group
v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
label-for="prometheus-graphs-dropdown-buttons"
class="dropdown-buttons col-lg d-lg-flex align-items-end"
class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end"
>
<div id="prometheus-graphs-dropdown-buttons">
<gl-button
......
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
getTimeDiff,
getTimeWindow,
stringToISODate,
ISODateToString,
truncateZerosInDateTime,
isDateTimePickerInputValid,
} from '~/monitoring/utils';
import { timeWindows } from '~/monitoring/constants';
export default {
components: {
Icon,
DateTimePickerInput,
GlFormGroup,
GlButton,
GlDropdown,
GlDropdownItem,
},
props: {
timeWindows: {
type: Object,
required: false,
default: () => timeWindows,
},
selectedTimeWindow: {
type: Object,
required: false,
default: () => {},
},
},
data() {
return {
selectedTimeWindowText: '',
customTime: {
from: null,
to: null,
},
};
},
computed: {
applyEnabled() {
return Boolean(this.inputState.from && this.inputState.to);
},
inputState() {
const { from, to } = this.customTime;
return {
from: from && isDateTimePickerInputValid(from),
to: to && isDateTimePickerInputValid(to),
};
},
},
mounted() {
const range = getTimeWindow(this.selectedTimeWindow);
if (range) {
this.selectedTimeWindowText = this.timeWindows[range];
} else {
this.customTime = {
from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
};
this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
}
},
methods: {
activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindowText;
},
setCustomTimeWindowParameter() {
this.$emit('onApply', {
start: stringToISODate(this.customTime.from),
end: stringToISODate(this.customTime.to),
});
},
setTimeWindowParameter(key) {
const { start, end } = getTimeDiff(key);
this.$emit('onApply', {
start,
end,
});
},
closeDropdown() {
this.$refs.dropdown.hide();
},
},
};
</script>
<template>
<gl-dropdown
ref="dropdown"
:text="selectedTimeWindowText"
menu-class="time-window-dropdown-menu"
class="js-time-window-dropdown"
>
<div class="d-flex justify-content-between time-window-dropdown-menu-container">
<gl-form-group
:label="__('Custom range')"
label-for="custom-from-time"
class="custom-time-range-form-group col-md-7 p-0 m-0"
>
<date-time-picker-input
id="custom-time-from"
v-model="customTime.from"
:label="__('From')"
:state="inputState.from"
/>
<date-time-picker-input
id="custom-time-to"
v-model="customTime.to"
:label="__('To')"
:state="inputState.to"
/>
<gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button
variant="success"
:disabled="!applyEnabled"
@click="setCustomTimeWindowParameter"
>{{ __('Apply') }}</gl-button
>
</gl-form-group>
</gl-form-group>
<gl-form-group
:label="__('Quick range')"
label-for="group-id-dropdown"
label-align="center"
class="col-md-4 p-0 m-0"
>
<gl-dropdown-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
active-class="active"
@click="setTimeWindowParameter(key)"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: !activeTimeWindow(key) }"
/>
{{ value }}
</gl-dropdown-item>
</gl-form-group>
</div>
</gl-dropdown>
</template>
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { dateFormats } from '~/monitoring/constants';
const inputGroupText = {
invalidFeedback: sprintf(s__('Format: %{dateFormat}'), {
dateFormat: dateFormats.dateTimePicker.format,
}),
placeholder: dateFormats.dateTimePicker.format,
};
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>
......@@ -3,6 +3,11 @@ import { __ } from '~/locale';
export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300;
/**
* Valid strings for this regex are
* 2019-10-01 and 2019-10-01 01:02:03
*/
export 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]))?$/;
export const graphTypes = {
deploymentData: 'scatter',
......@@ -28,6 +33,11 @@ export const timeWindows = {
export const dateFormats = {
timeOfDay: 'h:MM TT',
default: 'dd mmm yyyy, h:MMTT',
dateTimePicker: {
format: 'yyyy-mm-dd hh:mm:ss',
ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'",
stringDate: 'yyyy-mm-dd HH:MM:ss',
},
};
export const secondsIn = {
......
import { secondsIn, timeWindowsKeyNames } from './constants';
import dateformat from 'dateformat';
import { secondsIn, dateTimePickerRegex, dateFormats } from './constants';
const secondsToMilliseconds = seconds => seconds * 1000;
......@@ -19,7 +20,49 @@ export const getTimeWindow = ({ start, end }) =>
return timeRange;
}
return acc;
}, timeWindowsKeyNames.eightHours);
}, null);
export const isDateTimePickerInputValid = val => dateTimePickerRegex.test(val);
export const truncateZerosInDateTime = datetime => datetime.replace(' 00:00:00', '');
/**
* The URL params start and end need to be validated
* before passing them down to other components.
*
* @param {string} dateString
*/
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 {
return false;
}
};
/**
* 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.dateTimePicker.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.dateTimePicker.stringDate);
/**
* This method is used to validate if the graph data format for a chart component
......
......@@ -46,6 +46,20 @@
}
}
.prometheus-graphs-header {
.time-window-dropdown-menu {
padding: $gl-padding $gl-padding 0 $gl-padding-12;
}
.time-window-dropdown-menu-container {
width: 360px;
}
.custom-time-range-form-group > label {
padding-bottom: $gl-padding;
}
}
.prometheus-panel {
margin-top: 20px;
}
......
---
title: Improve time window filtering on metrics dashboard
merge_request: 17554
author:
type: added
......@@ -250,6 +250,9 @@ msgstr ""
msgid "%{firstLabel} +%{labelCount} more"
msgstr ""
msgid "%{from} to %{to}"
msgstr ""
msgid "%{gitlab_ci_yml} not found in this commit"
msgstr ""
......@@ -1776,6 +1779,9 @@ msgstr ""
msgid "Applied"
msgstr ""
msgid "Apply"
msgstr ""
msgid "Apply a label"
msgstr ""
......@@ -4855,6 +4861,9 @@ msgstr ""
msgid "Custom project templates have not been set up for groups that you are a member of. They are enabled from a group’s settings page. Contact your group’s Owner or Maintainer to setup custom project templates."
msgstr ""
msgid "Custom range"
msgstr ""
msgid "CustomCycleAnalytics|Add a stage"
msgstr ""
......@@ -7331,6 +7340,9 @@ msgstr ""
msgid "Format"
msgstr ""
msgid "Format: %{dateFormat}"
msgstr ""
msgid "Forward external support email address to"
msgstr ""
......@@ -10412,6 +10424,9 @@ msgstr ""
msgid "Metrics|Legend label (optional)"
msgstr ""
msgid "Metrics|Link contains an invalid time window."
msgstr ""
msgid "Metrics|Max"
msgstr ""
......@@ -13379,6 +13394,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
msgid "Quick range"
msgstr ""
msgid "README"
msgstr ""
......@@ -17155,6 +17173,9 @@ msgstr ""
msgid "Titles and Filenames"
msgstr ""
msgid "To"
msgstr ""
msgid "To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration."
msgstr ""
......
import { mount } from '@vue/test-utils';
import DateTimePickerInput from '~/monitoring/components/date_time_picker/date_time_picker_input.vue';
const inputLabel = 'This is a label';
const inputValue = 'something';
describe('DateTimePickerInput', () => {
let wrapper;
const createComponent = (propsData = {}) => {
wrapper = mount(DateTimePickerInput, {
propsData: {
state: null,
value: '',
label: '',
...propsData,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders label above the input', () => {
createComponent({
label: inputLabel,
});
expect(wrapper.find('.gl-form-group label').text()).toBe(inputLabel);
});
it('renders the same `ID` for input and `for` for label', () => {
createComponent({ label: inputLabel });
expect(wrapper.find('.gl-form-group label').attributes('for')).toBe(
wrapper.find('input').attributes('id'),
);
});
it('renders valid input in gray color instead of green', () => {
createComponent({
state: true,
});
expect(wrapper.find('input').classes('is-valid')).toBe(false);
});
it('renders invalid input in red color', () => {
createComponent({
state: false,
});
expect(wrapper.find('input').classes('is-invalid')).toBe(true);
});
it('input event is emitted when focus is lost', () => {
createComponent();
jest.spyOn(wrapper.vm, '$emit');
wrapper.find('input').setValue(inputValue);
wrapper.find('input').trigger('blur');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
});
});
import { mount } from '@vue/test-utils';
import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue';
import { timeWindows } from '~/monitoring/constants';
const timeWindowsCount = Object.keys(timeWindows).length;
const selectedTimeWindow = {
start: '2019-10-10T07:00:00.000Z',
end: '2019-10-13T07:00:00.000Z',
};
const selectedTimeWindowText = `3 days`;
describe('DateTimePicker', () => {
let dateTimePicker;
const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle');
const dropdownMenu = () => dateTimePicker.find('.dropdown-menu');
const applyButtonElement = () => dateTimePicker.find('button[variant="success"]').element;
const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
const fillInputAndBlur = (input, val) => {
dateTimePicker.find(input).setValue(val);
dateTimePicker.find(input).trigger('blur');
};
const createComponent = props => {
dateTimePicker = mount(DateTimePicker, {
propsData: {
timeWindows,
selectedTimeWindow,
...props,
},
sync: false,
});
};
afterEach(() => {
dateTimePicker.destroy();
});
it('renders dropdown toggle button with selected text', done => {
createComponent();
dateTimePicker.vm.$nextTick(() => {
expect(dropdownToggle().text()).toBe(selectedTimeWindowText);
done();
});
});
it('renders dropdown with 2 custom time range inputs', () => {
createComponent();
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.findAll('input').length).toBe(2);
});
});
it('renders inputs with h/m/s truncated if its all 0s', done => {
createComponent({
selectedTimeWindow: {
start: '2019-10-10T00:00:00.000Z',
end: '2019-10-14T00:10:00.000Z',
},
});
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
expect(dateTimePicker.find('#custom-time-to').element.value).toBe('2019-10-14 00:10:00');
done();
});
});
it(`renders dropdown with ${timeWindowsCount} items in quick range`, done => {
createComponent();
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.findAll('.dropdown-item').length).toBe(timeWindowsCount);
done();
});
});
it(`renders dropdown with correct quick range item selected`, done => {
createComponent();
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(selectedTimeWindowText);
expect(dateTimePicker.find('.dropdown-item.active svg').isVisible()).toBe(true);
done();
});
});
it('renders a disabled apply button on load', () => {
createComponent();
expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
});
it('displays inline error message if custom time range inputs are invalid', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01abc');
fillInputAndBlur('#custom-time-to', '2019-10-10abc');
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
done();
});
});
it('keeps apply button disabled with invalid custom time range inputs', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01abc');
fillInputAndBlur('#custom-time-to', '2019-09-19');
dateTimePicker.vm.$nextTick(() => {
expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
done();
});
});
it('enables apply button with valid custom time range inputs', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01');
fillInputAndBlur('#custom-time-to', '2019-10-19');
dateTimePicker.vm.$nextTick(() => {
expect(applyButtonElement().getAttribute('disabled')).toBeNull();
done();
});
});
it('returns an object when apply is clicked', done => {
createComponent();
fillInputAndBlur('#custom-time-from', '2019-10-01');
fillInputAndBlur('#custom-time-to', '2019-10-19');
dateTimePicker.vm.$nextTick(() => {
jest.spyOn(dateTimePicker.vm, '$emit');
applyButtonElement().click();
expect(dateTimePicker.vm.$emit).toHaveBeenCalledWith('onApply', {
end: '2019-10-19T00:00:00Z',
start: '2019-10-01T00:00:00Z',
});
done();
});
});
it('hides the popover with cancel button', done => {
createComponent();
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
cancelButtonElement().click();
dateTimePicker.vm.$nextTick(() => {
expect(dropdownMenu().classes('show')).toBe(false);
done();
});
});
});
});
......@@ -4,7 +4,6 @@ import { GlToast } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
......@@ -37,6 +36,12 @@ const propsData = {
validateQueryPath: '',
};
const resetSpy = spy => {
if (spy) {
spy.calls.reset();
}
};
export default propsData;
describe('Dashboard', () => {
......@@ -96,10 +101,15 @@ describe('Dashboard', () => {
});
describe('requests information to the server', () => {
let spy;
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
afterEach(() => {
resetSpy(spy);
});
it('shows up a loading state', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
......@@ -272,7 +282,7 @@ describe('Dashboard', () => {
});
});
it('renders the time window dropdown with a set of options', done => {
it('renders the datetimepicker dropdown', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
......@@ -282,17 +292,9 @@ describe('Dashboard', () => {
},
store,
});
const numberOfTimeWindows = Object.keys(timeWindows).length;
setTimeout(() => {
const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
const timeWindowDropdownEls = component.$el.querySelectorAll(
'.js-time-window-dropdown .dropdown-item',
);
expect(timeWindowDropdown).not.toBeNull();
expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
expect(component.$el.querySelector('.js-time-window-dropdown')).not.toBeNull();
done();
});
});
......@@ -355,7 +357,7 @@ describe('Dashboard', () => {
});
});
it('defaults to the eight hours time window for non valid url parameters', done => {
it('shows an error message if invalid url parameters are passed', done => {
spyOnDependency(Dashboard, 'getParameterValues').and.returnValue([
'<script>alert("XSS")</script>',
]);
......@@ -366,9 +368,11 @@ describe('Dashboard', () => {
store,
});
Vue.nextTick(() => {
expect(component.selectedTimeWindowKey).toEqual(timeWindowsKeyNames.eightHours);
spy = spyOn(component, 'showInvalidDateError');
component.$mount();
component.$nextTick(() => {
expect(component.showInvalidDateError).toHaveBeenCalled();
done();
});
});
......
import { getTimeDiff, getTimeWindow, graphDataValidatorForValues } from '~/monitoring/utils';
import {
getTimeDiff,
getTimeWindow,
graphDataValidatorForValues,
isDateTimePickerInputValid,
truncateZerosInDateTime,
stringToISODate,
ISODateToString,
isValidDate,
} from '~/monitoring/utils';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
......@@ -57,7 +66,7 @@ describe('getTimeWindow', () => {
end: '2019-10-01T21:27:47.000Z',
},
],
expected: timeWindowsKeyNames.eightHours,
expected: null,
},
{
args: [
......@@ -66,7 +75,7 @@ describe('getTimeWindow', () => {
end: '',
},
],
expected: timeWindowsKeyNames.eightHours,
expected: null,
},
{
args: [
......@@ -75,11 +84,11 @@ describe('getTimeWindow', () => {
end: null,
},
],
expected: timeWindowsKeyNames.eightHours,
expected: null,
},
{
args: [{}],
expected: timeWindowsKeyNames.eightHours,
expected: null,
},
].forEach(({ args, expected }) => {
it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
......@@ -111,3 +120,190 @@ describe('graphDataValidatorForValues', () => {
expect(validGraphData).toBe(true);
});
});
describe('stringToISODate', () => {
['', 'null', undefined, 'abc'].forEach(input => {
it(`throws error for invalid input like ${input}`, done => {
try {
stringToISODate(input);
} catch (e) {
expect(e).toBeDefined();
done();
}
});
});
[
{
input: '2019-09-09 01:01:01',
output: '2019-09-09T01:01:01Z',
},
{
input: '2019-09-09 00:00:00',
output: '2019-09-09T00:00:00Z',
},
{
input: '2019-09-09 23:59:59',
output: '2019-09-09T23:59:59Z',
},
{
input: '2019-09-09',
output: '2019-09-09T00:00:00Z',
},
].forEach(({ input, output }) => {
it(`returns ${output} from ${input}`, () => {
expect(stringToISODate(input)).toBe(output);
});
});
});
describe('ISODateToString', () => {
[
{
input: new Date('2019-09-09T00:00:00.000Z'),
output: '2019-09-09 00:00:00',
},
{
input: new Date('2019-09-09T07:00:00.000Z'),
output: '2019-09-09 07:00:00',
},
].forEach(({ input, output }) => {
it(`ISODateToString return ${output} for ${input}`, () => {
expect(ISODateToString(input)).toBe(output);
});
});
});
describe('truncateZerosInDateTime', () => {
[
{
input: '',
output: '',
},
{
input: '2019-10-10',
output: '2019-10-10',
},
{
input: '2019-10-10 00:00:01',
output: '2019-10-10 00:00:01',
},
{
input: '2019-10-10 00:00:00',
output: '2019-10-10',
},
].forEach(({ input, output }) => {
it(`truncateZerosInDateTime return ${output} for ${input}`, () => {
expect(truncateZerosInDateTime(input)).toBe(output);
});
});
});
describe('isValidDate', () => {
[
{
input: '2019-09-09T00:00:00.000Z',
output: true,
},
{
input: '2019-09-09T000:00.000Z',
output: false,
},
{
input: 'a2019-09-09T000:00.000Z',
output: false,
},
{
input: '2019-09-09T',
output: false,
},
{
input: '2019-09-09',
output: true,
},
{
input: '2019-9-9',
output: true,
},
{
input: '2019-9-',
output: true,
},
{
input: '2019--',
output: false,
},
{
input: '2019',
output: true,
},
{
input: '',
output: false,
},
{
input: null,
output: false,
},
].forEach(({ input, output }) => {
it(`isValidDate return ${output} for ${input}`, () => {
expect(isValidDate(input)).toBe(output);
});
});
});
describe('isDateTimePickerInputValid', () => {
[
{
input: null,
output: false,
},
{
input: '',
output: false,
},
{
input: 'xxxx-xx-xx',
output: false,
},
{
input: '9999-99-19',
output: false,
},
{
input: '2019-19-23',
output: false,
},
{
input: '2019-09-23',
output: true,
},
{
input: '2019-09-23 x',
output: false,
},
{
input: '2019-09-29 0:0:0',
output: false,
},
{
input: '2019-09-29 00:00:00',
output: true,
},
{
input: '2019-09-29 24:24:24',
output: false,
},
{
input: '2019-09-29 23:24:24',
output: true,
},
{
input: '2019-09-29 23:24:24 ',
output: false,
},
].forEach(({ input, output }) => {
it(`returns ${output} for ${input}`, () => {
expect(isDateTimePickerInputValid(input)).toBe(output);
});
});
});
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