Skip to content

Commit 3e2c484

Browse files
authored
Round start and end values (#89030) (#90035)
When getting the start and end times, use the d3 time scale `ticks` function to round the start and end times. Example from a query: Before: ```json { "range": { "@timestamp": { "gte": 1611262874814, "lte": 1611263774814, "format": "epoch_millis" } } }, ``` After: ```json { "range": { "@timestamp": { "gte": 1611263040000, "lte": 1611263880000, "format": "epoch_millis" } } }, ``` The `ticks` function makes it so the amount of rounding is proportional to the size of the time range, so shorter time ranges will be rounded less. Also fix a bug where invalid ranges in the query string were not handled correctly. Fixes #84530.
1 parent 36ea213 commit 3e2c484

File tree

8 files changed

+176
-77
lines changed

8 files changed

+176
-77
lines changed

x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ describe('TraceLink', () => {
6060
describe('when no transaction is found', () => {
6161
it('renders a trace page', () => {
6262
jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
63+
rangeId: 0,
64+
refreshTimeRange: jest.fn(),
65+
uiFilters: {},
6366
urlParams: {
6467
rangeFrom: 'now-24h',
6568
rangeTo: 'now',
6669
},
67-
refreshTimeRange: jest.fn(),
68-
uiFilters: {},
6970
});
7071
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
7172
data: { transaction: undefined },
@@ -87,12 +88,13 @@ describe('TraceLink', () => {
8788
describe('transaction page', () => {
8889
beforeAll(() => {
8990
jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
91+
rangeId: 0,
92+
refreshTimeRange: jest.fn(),
93+
uiFilters: {},
9094
urlParams: {
9195
rangeFrom: 'now-24h',
9296
rangeTo: 'now',
9397
},
94-
refreshTimeRange: jest.fn(),
95-
uiFilters: {},
9698
});
9799
});
98100

x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ function MockUrlParamsProvider({
3131
return (
3232
<UrlParamsContext.Provider
3333
value={{
34-
urlParams,
34+
rangeId: 0,
3535
refreshTimeRange: mockRefreshTimeRange,
36+
urlParams,
3637
uiFilters: useUiFilters(urlParams),
3738
}}
3839
children={children}

x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,53 @@ import moment from 'moment-timezone';
99
import * as helpers from './helpers';
1010

1111
describe('url_params_context helpers', () => {
12-
describe('getParsedDate', () => {
13-
describe('given undefined', () => {
14-
it('returns undefined', () => {
15-
expect(helpers.getParsedDate(undefined)).toBeUndefined();
12+
describe('getDateRange', () => {
13+
describe('with non-rounded dates', () => {
14+
describe('one minute', () => {
15+
it('rounds the values', () => {
16+
expect(
17+
helpers.getDateRange({
18+
state: {},
19+
rangeFrom: '2021-01-28T05:47:52.134Z',
20+
rangeTo: '2021-01-28T05:48:55.304Z',
21+
})
22+
).toEqual({
23+
start: '2021-01-28T05:47:50.000Z',
24+
end: '2021-01-28T05:49:00.000Z',
25+
});
26+
});
1627
});
17-
});
18-
19-
describe('given a parsable date', () => {
20-
it('returns the parsed date', () => {
21-
expect(helpers.getParsedDate('1970-01-01T00:00:00.000Z')).toEqual(
22-
'1970-01-01T00:00:00.000Z'
23-
);
28+
describe('one day', () => {
29+
it('rounds the values', () => {
30+
expect(
31+
helpers.getDateRange({
32+
state: {},
33+
rangeFrom: '2021-01-27T05:46:07.377Z',
34+
rangeTo: '2021-01-28T05:46:13.367Z',
35+
})
36+
).toEqual({
37+
start: '2021-01-27T03:00:00.000Z',
38+
end: '2021-01-28T06:00:00.000Z',
39+
});
40+
});
2441
});
25-
});
2642

27-
describe('given a non-parsable date', () => {
28-
it('returns null', () => {
29-
expect(helpers.getParsedDate('nope')).toEqual(null);
43+
describe('one year', () => {
44+
it('rounds the values', () => {
45+
expect(
46+
helpers.getDateRange({
47+
state: {},
48+
rangeFrom: '2020-01-28T05:52:36.290Z',
49+
rangeTo: '2021-01-28T05:52:39.741Z',
50+
})
51+
).toEqual({
52+
start: '2020-01-01T00:00:00.000Z',
53+
end: '2021-02-01T00:00:00.000Z',
54+
});
55+
});
3056
});
3157
});
32-
});
3358

34-
describe('getDateRange', () => {
3559
describe('when rangeFrom and rangeTo are not changed', () => {
3660
it('returns the previous state', () => {
3761
expect(
@@ -52,6 +76,45 @@ describe('url_params_context helpers', () => {
5276
});
5377
});
5478

79+
describe('when rangeFrom or rangeTo are falsy', () => {
80+
it('returns the previous state', () => {
81+
// Disable console warning about not receiving a valid date for rangeFrom
82+
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
83+
84+
expect(
85+
helpers.getDateRange({
86+
state: {
87+
start: '1972-01-01T00:00:00.000Z',
88+
end: '1973-01-01T00:00:00.000Z',
89+
},
90+
rangeFrom: '',
91+
rangeTo: 'now',
92+
})
93+
).toEqual({
94+
start: '1972-01-01T00:00:00.000Z',
95+
end: '1973-01-01T00:00:00.000Z',
96+
});
97+
});
98+
});
99+
100+
describe('when the start or end are invalid', () => {
101+
it('returns the previous state', () => {
102+
expect(
103+
helpers.getDateRange({
104+
state: {
105+
start: '1972-01-01T00:00:00.000Z',
106+
end: '1973-01-01T00:00:00.000Z',
107+
},
108+
rangeFrom: 'nope',
109+
rangeTo: 'now',
110+
})
111+
).toEqual({
112+
start: '1972-01-01T00:00:00.000Z',
113+
end: '1973-01-01T00:00:00.000Z',
114+
});
115+
});
116+
});
117+
55118
describe('when rangeFrom or rangeTo have changed', () => {
56119
it('returns new state', () => {
57120
jest.spyOn(datemath, 'parse').mockReturnValue(moment(0).utc());

x-pack/plugins/apm/public/context/url_params_context/helpers.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { compact, pickBy } from 'lodash';
87
import datemath from '@elastic/datemath';
8+
import { scaleUtc } from 'd3-scale';
9+
import { compact, pickBy } from 'lodash';
910
import { IUrlParams } from './types';
1011

11-
export function getParsedDate(rawDate?: string, opts = {}) {
12+
function getParsedDate(rawDate?: string, options = {}) {
1213
if (rawDate) {
13-
const parsed = datemath.parse(rawDate, opts);
14-
if (parsed) {
15-
return parsed.toISOString();
14+
const parsed = datemath.parse(rawDate, options);
15+
if (parsed && parsed.isValid()) {
16+
return parsed.toDate();
1617
}
1718
}
1819
}
@@ -26,13 +27,27 @@ export function getDateRange({
2627
rangeFrom?: string;
2728
rangeTo?: string;
2829
}) {
30+
// If the previous state had the same range, just return that instead of calculating a new range.
2931
if (state.rangeFrom === rangeFrom && state.rangeTo === rangeTo) {
3032
return { start: state.start, end: state.end };
3133
}
3234

35+
const start = getParsedDate(rangeFrom);
36+
const end = getParsedDate(rangeTo, { roundUp: true });
37+
38+
// `getParsedDate` will return undefined for invalid or empty dates. We return
39+
// the previous state if either date is undefined.
40+
if (!start || !end) {
41+
return { start: state.start, end: state.end };
42+
}
43+
44+
// Calculate ticks for the time ranges to produce nicely rounded values.
45+
const ticks = scaleUtc().domain([start, end]).nice().ticks();
46+
47+
// Return the first and last tick values.
3348
return {
34-
start: getParsedDate(rangeFrom),
35-
end: getParsedDate(rangeTo, { roundUp: true }),
49+
start: ticks[0].toISOString(),
50+
end: ticks[ticks.length - 1].toISOString(),
3651
};
3752
}
3853

x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ export function MockUrlParamsContextProvider({
3030
return (
3131
<UrlParamsContext.Provider
3232
value={{
33-
urlParams,
33+
rangeId: 0,
3434
refreshTimeRange,
35+
urlParams,
3536
uiFilters: useUiFilters(urlParams),
3637
}}
3738
children={children}

x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import * as React from 'react';
8-
import { UrlParamsContext, UrlParamsProvider } from './url_params_context';
7+
import { waitFor } from '@testing-library/react';
98
import { mount } from 'enzyme';
10-
import { Location, History } from 'history';
11-
import { MemoryRouter, Router } from 'react-router-dom';
9+
import { History, Location } from 'history';
1210
import moment from 'moment-timezone';
11+
import * as React from 'react';
12+
import { MemoryRouter, Router } from 'react-router-dom';
1313
import { IUrlParams } from './types';
14-
import { getParsedDate } from './helpers';
15-
import { waitFor } from '@testing-library/react';
14+
import { UrlParamsContext, UrlParamsProvider } from './url_params_context';
1615

1716
function mountParams(location: Location) {
1817
return mount(
@@ -33,11 +32,11 @@ function getDataFromOutput(wrapper: ReturnType<typeof mount>) {
3332
}
3433

3534
describe('UrlParamsContext', () => {
36-
beforeEach(() => {
35+
beforeAll(() => {
3736
moment.tz.setDefault('Etc/GMT');
3837
});
3938

40-
afterEach(() => {
39+
afterAll(() => {
4140
moment.tz.setDefault('');
4241
});
4342

@@ -50,8 +49,11 @@ describe('UrlParamsContext', () => {
5049

5150
const wrapper = mountParams(location);
5251
const params = getDataFromOutput(wrapper);
53-
expect(params.start).toEqual('2010-03-15T12:00:00.000Z');
54-
expect(params.end).toEqual('2010-04-10T12:00:00.000Z');
52+
53+
expect([params.start, params.end]).toEqual([
54+
'2010-03-15T00:00:00.000Z',
55+
'2010-04-11T00:00:00.000Z',
56+
]);
5557
});
5658

5759
it('should update param values if location has changed', () => {
@@ -66,8 +68,11 @@ describe('UrlParamsContext', () => {
6668
// force an update
6769
wrapper.setProps({ abc: 123 });
6870
const params = getDataFromOutput(wrapper);
69-
expect(params.start).toEqual('2009-03-15T12:00:00.000Z');
70-
expect(params.end).toEqual('2009-04-10T12:00:00.000Z');
71+
72+
expect([params.start, params.end]).toEqual([
73+
'2009-03-15T00:00:00.000Z',
74+
'2009-04-11T00:00:00.000Z',
75+
]);
7176
});
7277

7378
it('should parse relative time ranges on mount', () => {
@@ -76,13 +81,20 @@ describe('UrlParamsContext', () => {
7681
search: '?rangeFrom=now-1d%2Fd&rangeTo=now-1d%2Fd&transactionId=UPDATED',
7782
} as Location;
7883

84+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(0);
85+
7986
const wrapper = mountParams(location);
8087

8188
// force an update
8289
wrapper.setProps({ abc: 123 });
8390
const params = getDataFromOutput(wrapper);
84-
expect(params.start).toEqual(getParsedDate('now-1d/d'));
85-
expect(params.end).toEqual(getParsedDate('now-1d/d', { roundUp: true }));
91+
92+
expect([params.start, params.end]).toEqual([
93+
'1969-12-31T00:00:00.000Z',
94+
'1970-01-01T00:00:00.000Z',
95+
]);
96+
97+
nowSpy.mockRestore();
8698
});
8799

88100
it('should refresh the time range with new values', async () => {
@@ -130,8 +142,11 @@ describe('UrlParamsContext', () => {
130142
expect(calls.length).toBe(2);
131143

132144
const params = getDataFromOutput(wrapper);
133-
expect(params.start).toEqual('2005-09-20T12:00:00.000Z');
134-
expect(params.end).toEqual('2005-10-21T12:00:00.000Z');
145+
146+
expect([params.start, params.end]).toEqual([
147+
'2005-09-19T00:00:00.000Z',
148+
'2005-10-23T00:00:00.000Z',
149+
]);
135150
});
136151

137152
it('should refresh the time range with new values if time range is relative', async () => {
@@ -177,7 +192,10 @@ describe('UrlParamsContext', () => {
177192
await waitFor(() => {});
178193

179194
const params = getDataFromOutput(wrapper);
180-
expect(params.start).toEqual('2000-06-14T00:00:00.000Z');
181-
expect(params.end).toEqual('2000-06-14T23:59:59.999Z');
195+
196+
expect([params.start, params.end]).toEqual([
197+
'2000-06-14T00:00:00.000Z',
198+
'2000-06-15T00:00:00.000Z',
199+
]);
182200
});
183201
});

0 commit comments

Comments
 (0)