Commit 4816319c authored by Kushal Pandya's avatar Kushal Pandya

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

Use time ranges in date picker (utils)

See merge request gitlab-org/gitlab!23765
parents c785530c 15761a33
import dateformat from 'dateformat';
import { pick, omit, isEqual, isEmpty } from 'lodash';
import { secondsToMilliseconds } from './datetime_utility';
const MINIMUM_DATE = new Date(0);
......@@ -221,3 +222,99 @@ export function getRangeType(range) {
*/
export const convertToFixedRange = dateTimeRange =>
handlers[getRangeType(dateTimeRange)](dateTimeRange);
/**
* Returns a copy of the object only with time range
* properties relevant to time range calculation.
*
* Filtered properties are:
* - 'start'
* - 'end'
* - 'anchor'
* - 'duration'
* - 'direction': if direction is already the default, its removed.
*
* @param {Object} timeRange - A time range object
* @returns Copy of time range
*/
const pruneTimeRange = timeRange => {
const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']);
if (res.direction === DEFAULT_DIRECTION) {
return omit(res, 'direction');
}
return res;
};
/**
* Returns true if the time ranges are equal according to
* the time range calculation properties
*
* @param {Object} timeRange - A time range object
* @param {Object} other - Time range object to compare with.
* @returns true if the time ranges are equal, false otherwise
*/
export const isEqualTimeRanges = (timeRange, other) => {
const tr1 = pruneTimeRange(timeRange);
const tr2 = pruneTimeRange(other);
return isEqual(tr1, tr2);
};
/**
* Searches for a time range in a array of time ranges using
* only the properies relevant to time ranges calculation.
*
* @param {Object} timeRange - Time range to search (needle)
* @param {Array} timeRanges - Array of time tanges (haystack)
*/
export const findTimeRange = (timeRange, timeRanges) =>
timeRanges.find(element => isEqualTimeRanges(element, timeRange));
// Time Ranges as URL Parameters Utils
/**
* List of possible time ranges parameters
*/
export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds', 'direction'];
/**
* Converts a valid time range to a flat key-value pairs object.
*
* Duration is flatted to avoid having nested objects.
*
* @param {Object} A time range
* @returns key-value pairs object that can be used as parameters in a URL.
*/
export const timeRangeToParams = timeRange => {
let params = pruneTimeRange(timeRange);
if (timeRange.duration) {
const durationParms = {};
Object.keys(timeRange.duration).forEach(key => {
durationParms[`duration_${key}`] = timeRange.duration[key].toString();
});
params = { ...durationParms, ...params };
params = omit(params, 'duration');
}
return params;
};
/**
* Converts a valid set of flat params to a time range object
*
* Parameters that are not part of time range object are ignored.
*
* @param {params} params - key-value pairs object.
*/
export const timeRangeFromParams = params => {
const timeRangeParams = pick(params, timeRangeParamNames);
let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => {
// unflatten duration
if (key.startsWith('duration_')) {
acc.duration = acc.duration || {};
acc.duration[key.slice('duration_'.length)] = parseInt(val, 10);
return acc;
}
return { [key]: val, ...acc };
}, {});
range = pruneTimeRange(range);
return !isEmpty(range) ? range : null;
};
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import {
timeRangeParamNames,
timeRangeFromParams,
timeRangeToParams,
} from '~/lib/utils/datetime_range';
/**
* This method is used to validate if the graph data format for a chart component
* that needs a time series as a response from a prometheus query (query_range) is
......@@ -93,4 +100,35 @@ export const graphDataValidatorForAnomalyValues = graphData => {
);
};
/**
* Returns a time range from the current URL params
*
* @returns {Object} The time range defined by the
* current URL, reading from `window.location.search`
*/
export const timeRangeFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
return timeRangeFromParams(params);
};
/**
* Returns a URL with no time range based on the current URL.
*
* @param {String} New URL
*/
export const removeTimeRangeParams = (url = window.location.href) =>
removeParams(timeRangeParamNames, url);
/**
* Returns a URL for the a different time range based on the
* current URL and a time range.
*
* @param {String} New URL
*/
export const timeRangeToUrl = (timeRange, url = window.location.href) => {
const toUrl = removeTimeRangeParams(url);
const params = timeRangeToParams(timeRange);
return mergeUrlParams(params, toUrl);
};
export default {};
import _ from 'lodash';
import { getRangeType, convertToFixedRange } from '~/lib/utils/datetime_range';
import {
getRangeType,
convertToFixedRange,
isEqualTimeRanges,
findTimeRange,
timeRangeToParams,
timeRangeFromParams,
} from '~/lib/utils/datetime_range';
const MOCK_NOW = Date.UTC(2020, 0, 23, 20);
const MOCK_NOW_ISO_STRING = new Date(MOCK_NOW).toISOString();
const mockFixedRange = {
label: 'January 2020',
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-31T23:59:00.000Z',
};
const mockAnchoredRange = {
label: 'First two minutes of 2020',
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
duration: {
seconds: 60 * 2,
},
};
const mockRollingRange = {
label: 'Next 2 minutes',
direction: 'after',
duration: {
seconds: 60 * 2,
},
};
const mockOpenRange = {
label: '2020 so far',
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
};
describe('Date time range utils', () => {
describe('getRangeType', () => {
it('infers correctly the range type from the input object', () => {
......@@ -43,38 +79,28 @@ describe('Date time range utils', () => {
});
describe('When a fixed range is input', () => {
const defaultFixedRange = {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-31T23:59:00.000Z',
label: 'January 2020',
};
const mockFixedRange = params => ({ ...defaultFixedRange, ...params });
it('converts a fixed range to an equal fixed range', () => {
const aFixedRange = mockFixedRange();
expect(convertToFixedRange(aFixedRange)).toEqual({
start: defaultFixedRange.start,
end: defaultFixedRange.end,
expect(convertToFixedRange(mockFixedRange)).toEqual({
start: mockFixedRange.start,
end: mockFixedRange.end,
});
});
it('throws an error when fixed range does not contain an end time', () => {
const aFixedRangeMissingEnd = _.omit(mockFixedRange(), 'end');
const aFixedRangeMissingEnd = _.omit(mockFixedRange, 'end');
expect(() => convertToFixedRange(aFixedRangeMissingEnd)).toThrow();
});
it('throws an error when fixed range does not contain a start time', () => {
const aFixedRangeMissingStart = _.omit(mockFixedRange(), 'start');
const aFixedRangeMissingStart = _.omit(mockFixedRange, 'start');
expect(() => convertToFixedRange(aFixedRangeMissingStart)).toThrow();
});
it('throws an error when the dates cannot be parsed', () => {
const wrongStart = mockFixedRange({ start: 'I_CANNOT_BE_PARSED' });
const wrongEnd = mockFixedRange({ end: 'I_CANNOT_BE_PARSED' });
const wrongStart = { ...mockFixedRange, start: 'I_CANNOT_BE_PARSED' };
const wrongEnd = { ...mockFixedRange, end: 'I_CANNOT_BE_PARSED' };
expect(() => convertToFixedRange(wrongStart)).toThrow();
expect(() => convertToFixedRange(wrongEnd)).toThrow();
......@@ -82,97 +108,61 @@ describe('Date time range utils', () => {
});
describe('When an anchored range is input', () => {
const defaultAnchoredRange = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
duration: {
seconds: 60 * 2,
},
label: 'First two minutes of 2020',
};
const mockAnchoredRange = params => ({ ...defaultAnchoredRange, ...params });
it('converts to a fixed range', () => {
const anAnchoredRange = mockAnchoredRange();
expect(convertToFixedRange(anAnchoredRange)).toEqual({
expect(convertToFixedRange(mockAnchoredRange)).toEqual({
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T00:02:00.000Z',
});
});
it('converts to a fixed range with a `before` direction', () => {
const anAnchoredRange = mockAnchoredRange({ direction: 'before' });
expect(convertToFixedRange(anAnchoredRange)).toEqual({
expect(convertToFixedRange({ ...mockAnchoredRange, direction: 'before' })).toEqual({
start: '2019-12-31T23:58:00.000Z',
end: '2020-01-01T00:00:00.000Z',
});
});
it('converts to a fixed range without an explicit direction, defaulting to `before`', () => {
const anAnchoredRange = _.omit(mockAnchoredRange(), 'direction');
const defaultDirectionRange = _.omit(mockAnchoredRange, 'direction');
expect(convertToFixedRange(anAnchoredRange)).toEqual({
expect(convertToFixedRange(defaultDirectionRange)).toEqual({
start: '2019-12-31T23:58:00.000Z',
end: '2020-01-01T00:00:00.000Z',
});
});
it('throws an error when the anchor cannot be parsed', () => {
const wrongAnchor = mockAnchoredRange({ anchor: 'I_CANNOT_BE_PARSED' });
const wrongAnchor = { ...mockAnchoredRange, anchor: 'I_CANNOT_BE_PARSED' };
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
});
describe('when a rolling range is input', () => {
it('converts to a fixed range', () => {
const aRollingRange = {
direction: 'after',
duration: {
seconds: 60 * 2,
},
label: 'Next 2 minutes',
};
expect(convertToFixedRange(aRollingRange)).toEqual({
expect(convertToFixedRange(mockRollingRange)).toEqual({
start: '2020-01-23T20:00:00.000Z',
end: '2020-01-23T20:02:00.000Z',
});
});
it('converts to a fixed range with an implicit `before` direction', () => {
const aRollingRangeWithNoDirection = {
duration: {
seconds: 60 * 2,
},
label: 'Last 2 minutes',
};
const noDirection = _.omit(mockRollingRange, 'direction');
expect(convertToFixedRange(aRollingRangeWithNoDirection)).toEqual({
expect(convertToFixedRange(noDirection)).toEqual({
start: '2020-01-23T19:58:00.000Z',
end: '2020-01-23T20:00:00.000Z',
});
});
it('throws an error when the duration is not in the right format', () => {
const wrongDuration = {
direction: 'before',
duration: {
minutes: 20,
},
label: 'Last 20 minutes',
};
const wrongDuration = { ...mockRollingRange, duration: { minutes: 20 } };
expect(() => convertToFixedRange(wrongDuration)).toThrow();
});
it('throws an error when the anchor is not valid', () => {
const wrongAnchor = {
anchor: 'CAN_T_PARSE_THIS',
direction: 'after',
label: '2020 so far',
};
const wrongAnchor = { ...mockRollingRange, anchor: 'CAN_T_PARSE_THIS' };
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
......@@ -180,51 +170,212 @@ describe('Date time range utils', () => {
describe('when an open range is input', () => {
it('converts to a fixed range with an `after` direction', () => {
const soFar2020 = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
label: '2020 so far',
};
expect(convertToFixedRange(soFar2020)).toEqual({
expect(convertToFixedRange(mockOpenRange)).toEqual({
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-23T20:00:00.000Z',
});
});
it('converts to a fixed range with the explicit `before` direction', () => {
const before2020 = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'before',
label: 'Before 2020',
};
const beforeOpenRange = { ...mockOpenRange, direction: 'before' };
expect(convertToFixedRange(before2020)).toEqual({
expect(convertToFixedRange(beforeOpenRange)).toEqual({
start: '1970-01-01T00:00:00.000Z',
end: '2020-01-01T00:00:00.000Z',
});
});
it('converts to a fixed range with the implicit `before` direction', () => {
const alsoBefore2020 = {
anchor: '2020-01-01T00:00:00.000Z',
label: 'Before 2020',
};
const noDirectionOpenRange = _.omit(mockOpenRange, 'direction');
expect(convertToFixedRange(alsoBefore2020)).toEqual({
expect(convertToFixedRange(noDirectionOpenRange)).toEqual({
start: '1970-01-01T00:00:00.000Z',
end: '2020-01-01T00:00:00.000Z',
});
});
it('throws an error when the anchor cannot be parsed', () => {
const wrongAnchor = {
anchor: 'CAN_T_PARSE_THIS',
const wrongAnchor = { ...mockOpenRange, anchor: 'CAN_T_PARSE_THIS' };
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
});
});
});
describe('isEqualTimeRanges', () => {
it('equal only compares relevant properies', () => {
expect(
isEqualTimeRanges(
{
...mockFixedRange,
label: 'A label',
default: true,
},
{
...mockFixedRange,
label: 'Another label',
default: false,
anotherKey: 'anotherValue',
},
),
).toBe(true);
expect(
isEqualTimeRanges(
{
...mockAnchoredRange,
label: 'A label',
default: true,
},
{
...mockAnchoredRange,
anotherKey: 'anotherValue',
},
),
).toBe(true);
});
});
describe('findTimeRange', () => {
const timeRanges = [
{
label: 'Before 2020',
anchor: '2020-01-01T00:00:00.000Z',
},
{
label: 'Last 30 minutes',
duration: { seconds: 60 * 30 },
},
{
label: 'In 2019',
start: '2019-01-01T00:00:00.000Z',
end: '2019-12-31T12:59:59.999Z',
},
{
label: 'Next 2 minutes',
direction: 'after',
label: '2020 so far',
duration: {
seconds: 60 * 2,
},
},
];
it('finds a time range', () => {
const tr0 = {
anchor: '2020-01-01T00:00:00.000Z',
};
expect(findTimeRange(tr0, timeRanges)).toBe(timeRanges[0]);
expect(() => convertToFixedRange(wrongAnchor)).toThrow();
const tr1 = {
duration: { seconds: 60 * 30 },
};
expect(findTimeRange(tr1, timeRanges)).toBe(timeRanges[1]);
const tr1Direction = {
direction: 'before',
duration: {
seconds: 60 * 30,
},
};
expect(findTimeRange(tr1Direction, timeRanges)).toBe(timeRanges[1]);
const tr2 = {
someOtherLabel: 'Added arbitrarily',
start: '2019-01-01T00:00:00.000Z',
end: '2019-12-31T12:59:59.999Z',
};
expect(findTimeRange(tr2, timeRanges)).toBe(timeRanges[2]);
const tr3 = {
direction: 'after',
duration: {
seconds: 60 * 2,
},
};
expect(findTimeRange(tr3, timeRanges)).toBe(timeRanges[3]);
});
it('doesnot finds a missing time range', () => {
const nonExistant = {
direction: 'before',
duration: {
seconds: 200,
},
};
expect(findTimeRange(nonExistant, timeRanges)).toBeUndefined();
});
});
describe('conversion to/from params', () => {
const mockFixedParams = {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-31T23:59:00.000Z',
};
const mockAnchoredParams = {
anchor: '2020-01-01T00:00:00.000Z',
direction: 'after',
duration_seconds: '120',
};
const mockRollingParams = {
direction: 'after',
duration_seconds: '120',
};
describe('timeRangeToParams', () => {
it('converts fixed ranges to params', () => {
expect(timeRangeToParams(mockFixedRange)).toEqual(mockFixedParams);
});
it('converts anchored ranges to params', () => {
expect(timeRangeToParams(mockAnchoredRange)).toEqual(mockAnchoredParams);
});
it('converts rolling ranges to params', () => {
expect(timeRangeToParams(mockRollingRange)).toEqual(mockRollingParams);
});
});
describe('timeRangeFromParams', () => {
it('converts fixed ranges from params', () => {
const params = { ...mockFixedParams, other_param: 'other_value' };
const expectedRange = _.omit(mockFixedRange, 'label');
expect(timeRangeFromParams(params)).toEqual(expectedRange);
});
it('converts anchored ranges to params', () => {
const expectedRange = _.omit(mockRollingRange, 'label');
expect(timeRangeFromParams(mockRollingParams)).toEqual(expectedRange);
});
it('converts rolling ranges from params', () => {
const params = { ...mockRollingParams, other_param: 'other_value' };
const expectedRange = _.omit(mockRollingRange, 'label');
expect(timeRangeFromParams(params)).toEqual(expectedRange);
});
it('converts rolling ranges from params with a default direction', () => {
const params = {
...mockRollingParams,
direction: 'before',
other_param: 'other_value',
};
const expectedRange = _.omit(mockRollingRange, 'label', 'direction');
expect(timeRangeFromParams(params)).toEqual(expectedRange);
});
it('converts to null when for no relevant params', () => {
const range = {
useless_param_1: 'value1',
useless_param_2: 'value2',
};
expect(timeRangeFromParams(range)).toBe(null);
});
});
});
......
import * as monitoringUtils from '~/monitoring/utils';
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import {
mockHost,
mockProjectDir,
graphDataPrometheusQuery,
graphDataPrometheusQueryRange,
anomalyMockGraphData,
} from './mock_data';
jest.mock('~/lib/utils/url_utility');
const mockPath = `${mockHost}${mockProjectDir}/-/environments/29/metrics`;
const generatedLink = 'http://chart.link.com';
const chartTitle = 'Some metric chart';
const range = {
start: '2019-01-01T00:00:00.000Z',
end: '2019-01-10T00:00:00.000Z',
};
const rollingRange = {
duration: { seconds: 120 },
};
describe('monitoring/utils', () => {
const generatedLink = 'http://chart.link.com';
const chartTitle = 'Some metric chart';
afterEach(() => {
mergeUrlParams.mockReset();
queryToObject.mockReset();
});
describe('trackGenerateLinkToChartEventOptions', () => {
it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
......@@ -117,4 +139,75 @@ describe('monitoring/utils', () => {
expect(monitoringUtils.graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false);
});
});
describe('timeRangeFromUrl', () => {
const { timeRangeFromUrl } = monitoringUtils;
it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
queryToObject.mockReturnValueOnce(range);
expect(timeRangeFromUrl()).toEqual(range);
});
it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
const { seconds } = rollingRange.duration;
queryToObject.mockReturnValueOnce({
dashboard: '.gitlab/dashboard/my_dashboard.yml',
duration_seconds: `${seconds}`,
});
expect(timeRangeFromUrl()).toEqual(rollingRange);
});
it('returns null when no time range paramters are given', () => {
const params = {
dashboard: '.gitlab/dashboards/custom_dashboard.yml',
param1: 'value1',
param2: 'value2',
};
expect(timeRangeFromUrl(params, mockPath)).toBe(null);
});
});
describe('removeTimeRangeParams', () => {
const { removeTimeRangeParams } = monitoringUtils;
it('returns when query contains `start` and `end` paramters are given', () => {
removeParams.mockReturnValueOnce(mockPath);
expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
mockPath,
);
});
});
describe('timeRangeToUrl', () => {
const { timeRangeToUrl } = monitoringUtils;
it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
const fromUrl = mockPath;
removeParams.mockReturnValueOnce(fromUrl);
mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(range)).toEqual(toUrl);
expect(mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
});
it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
const { seconds } = rollingRange.duration;
const toUrl = `${mockPath}?duration_seconds=${seconds}`;
const fromUrl = mockPath;
removeParams.mockReturnValueOnce(fromUrl);
mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: `${seconds}` }, fromUrl);
});
});
});
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