Skip to content

Commit 11e85c4

Browse files
committed
refactor logic into custom hook and add tests
1 parent 9e08540 commit 11e85c4

File tree

3 files changed

+225
-64
lines changed

3 files changed

+225
-64
lines changed

x-pack/plugins/lens/public/app_plugin/app.test.tsx

Lines changed: 136 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ReactWrapper } from 'enzyme';
1010
import { act } from 'react-dom/test-utils';
1111
import { App } from './app';
1212
import { LensAppProps, LensAppServices } from './types';
13-
import { EditorFrameInstance } from '../types';
13+
import { EditorFrameInstance, EditorFrameProps } from '../types';
1414
import { Document } from '../persistence';
1515
import { DOC_TYPE } from '../../common';
1616
import { mount } from 'enzyme';
@@ -44,6 +44,8 @@ import {
4444
import { LensAttributeService } from '../lens_attribute_service';
4545
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
4646
import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public';
47+
import { NativeRenderer } from '../native_renderer';
48+
import moment from 'moment';
4749

4850
jest.mock('../editor_frame_service/editor_frame/expression_helpers');
4951
jest.mock('src/core/public');
@@ -144,6 +146,11 @@ function createMockTimefilter() {
144146
return unsubscribe;
145147
},
146148
}),
149+
calculateBounds: jest.fn(() => ({
150+
min: moment('2021-01-10T04:00:00.000Z'),
151+
max: moment('2021-01-10T08:00:00.000Z'),
152+
})),
153+
getBounds: jest.fn(() => timeFilter),
147154
getRefreshInterval: () => {},
148155
getRefreshIntervalDefaults: () => {},
149156
getAutoRefreshFetch$: () => ({
@@ -233,6 +240,9 @@ describe('Lens App', () => {
233240
}),
234241
},
235242
search: createMockSearchService(),
243+
nowProvider: {
244+
get: jest.fn(),
245+
},
236246
} as unknown) as DataPublicPluginStart,
237247
storage: {
238248
get: jest.fn(),
@@ -306,8 +316,8 @@ describe('Lens App', () => {
306316
/>,
307317
Object {
308318
"dateRange": Object {
309-
"fromDate": "now-7d",
310-
"toDate": "now",
319+
"fromDate": "2021-01-10T04:00:00.000Z",
320+
"toDate": "2021-01-10T08:00:00.000Z",
311321
},
312322
"doc": undefined,
313323
"filters": Array [],
@@ -350,7 +360,7 @@ describe('Lens App', () => {
350360
expect(frame.mount).toHaveBeenCalledWith(
351361
expect.any(Element),
352362
expect.objectContaining({
353-
dateRange: { fromDate: 'now-7d', toDate: 'now' },
363+
dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
354364
query: { query: '', language: 'kuery' },
355365
filters: [pinnedFilter],
356366
})
@@ -1008,7 +1018,7 @@ describe('Lens App', () => {
10081018
expect(frame.mount).toHaveBeenCalledWith(
10091019
expect.any(Element),
10101020
expect.objectContaining({
1011-
dateRange: { fromDate: 'now-7d', toDate: 'now' },
1021+
dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
10121022
query: { query: '', language: 'kuery' },
10131023
})
10141024
);
@@ -1055,7 +1065,11 @@ describe('Lens App', () => {
10551065
});
10561066

10571067
it('updates the editor frame when the user changes query or time in the search bar', () => {
1058-
const { component, frame } = mountWith({});
1068+
const { component, frame, services } = mountWith({});
1069+
(services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({
1070+
min: moment('2021-01-09T04:00:00.000Z'),
1071+
max: moment('2021-01-09T08:00:00.000Z'),
1072+
});
10591073
act(() =>
10601074
component.find(TopNavMenu).prop('onQuerySubmit')!({
10611075
dateRange: { from: 'now-14d', to: 'now-7d' },
@@ -1071,10 +1085,14 @@ describe('Lens App', () => {
10711085
}),
10721086
{}
10731087
);
1088+
expect(services.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({
1089+
from: 'now-14d',
1090+
to: 'now-7d',
1091+
});
10741092
expect(frame.mount).toHaveBeenCalledWith(
10751093
expect.any(Element),
10761094
expect.objectContaining({
1077-
dateRange: { fromDate: 'now-14d', toDate: 'now-7d' },
1095+
dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' },
10781096
query: { query: 'new', language: 'lucene' },
10791097
})
10801098
);
@@ -1237,6 +1255,34 @@ describe('Lens App', () => {
12371255
);
12381256
});
12391257

1258+
it('clears all existing unpinned filters when the active saved query is cleared', () => {
1259+
const { component, frame, services } = mountWith({});
1260+
act(() =>
1261+
component.find(TopNavMenu).prop('onQuerySubmit')!({
1262+
dateRange: { from: 'now-14d', to: 'now-7d' },
1263+
query: { query: 'new', language: 'lucene' },
1264+
})
1265+
);
1266+
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
1267+
const field = ({ name: 'myfield' } as unknown) as IFieldType;
1268+
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
1269+
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
1270+
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
1271+
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
1272+
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
1273+
component.update();
1274+
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
1275+
component.update();
1276+
expect(frame.mount).toHaveBeenLastCalledWith(
1277+
expect.any(Element),
1278+
expect.objectContaining({
1279+
filters: [pinned],
1280+
})
1281+
);
1282+
});
1283+
});
1284+
1285+
describe('search session id management', () => {
12401286
it('updates the searchSessionId when the query is updated', () => {
12411287
const { component, frame } = mountWith({});
12421288
act(() => {
@@ -1263,12 +1309,12 @@ describe('Lens App', () => {
12631309
expect(frame.mount).toHaveBeenCalledWith(
12641310
expect.any(Element),
12651311
expect.objectContaining({
1266-
searchSessionId: `sessionId-1`,
1312+
searchSessionId: `sessionId-2`,
12671313
})
12681314
);
12691315
});
12701316

1271-
it('clears all existing unpinned filters when the active saved query is cleared', () => {
1317+
it('updates the searchSessionId when the active saved query is cleared', () => {
12721318
const { component, frame, services } = mountWith({});
12731319
act(() =>
12741320
component.find(TopNavMenu).prop('onQuerySubmit')!({
@@ -1286,31 +1332,67 @@ describe('Lens App', () => {
12861332
component.update();
12871333
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
12881334
component.update();
1289-
expect(frame.mount).toHaveBeenLastCalledWith(
1335+
expect(frame.mount).toHaveBeenCalledWith(
12901336
expect.any(Element),
12911337
expect.objectContaining({
1292-
filters: [pinned],
1338+
searchSessionId: `sessionId-2`,
12931339
})
12941340
);
12951341
});
12961342

1297-
it('updates the searchSessionId when the active saved query is cleared', () => {
1298-
const { component, frame, services } = mountWith({});
1299-
act(() =>
1300-
component.find(TopNavMenu).prop('onQuerySubmit')!({
1301-
dateRange: { from: 'now-14d', to: 'now-7d' },
1302-
query: { query: 'new', language: 'lucene' },
1343+
const mockUpdate = {
1344+
filterableIndexPatterns: [],
1345+
doc: {
1346+
title: '',
1347+
description: '',
1348+
visualizationType: '',
1349+
state: {
1350+
datasourceStates: {},
1351+
visualization: {},
1352+
filters: [],
1353+
query: { query: '', language: 'lucene' },
1354+
},
1355+
references: [],
1356+
},
1357+
isSaveable: true,
1358+
activeData: undefined,
1359+
};
1360+
1361+
it('does not update the searchSessionId when the state changes', () => {
1362+
const { component, frame } = mountWith({});
1363+
act(() => {
1364+
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
1365+
mockUpdate
1366+
);
1367+
});
1368+
component.update();
1369+
expect(frame.mount).not.toHaveBeenCalledWith(
1370+
expect.any(Element),
1371+
expect.objectContaining({
1372+
searchSessionId: `sessionId-2`,
13031373
})
13041374
);
1305-
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
1306-
const field = ({ name: 'myfield' } as unknown) as IFieldType;
1307-
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
1308-
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
1309-
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
1310-
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
1311-
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
1312-
component.update();
1313-
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
1375+
});
1376+
1377+
it('does update the searchSessionId when the state changes and too much time passed', () => {
1378+
const { component, frame, services } = mountWith({});
1379+
1380+
// time range is 100,000ms ago to 30,000ms ago (that's a lag of 30 percent)
1381+
(services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
1382+
(services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
1383+
from: 'now-2m',
1384+
to: 'now',
1385+
});
1386+
(services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
1387+
min: moment(Date.now() - 100000),
1388+
max: moment(Date.now() - 30000),
1389+
});
1390+
1391+
act(() => {
1392+
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
1393+
mockUpdate
1394+
);
1395+
});
13141396
component.update();
13151397
expect(frame.mount).toHaveBeenCalledWith(
13161398
expect.any(Element),
@@ -1319,6 +1401,34 @@ describe('Lens App', () => {
13191401
})
13201402
);
13211403
});
1404+
1405+
it('does not update the searchSessionId when the state changes and too little time has passed', () => {
1406+
const { component, frame, services } = mountWith({});
1407+
1408+
// time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update)
1409+
(services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300));
1410+
(services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
1411+
from: 'now-2m',
1412+
to: 'now',
1413+
});
1414+
(services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
1415+
min: moment(Date.now() - 100000),
1416+
max: moment(Date.now() - 300),
1417+
});
1418+
1419+
act(() => {
1420+
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
1421+
mockUpdate
1422+
);
1423+
});
1424+
component.update();
1425+
expect(frame.mount).not.toHaveBeenCalledWith(
1426+
expect.any(Element),
1427+
expect.objectContaining({
1428+
searchSessionId: `sessionId-2`,
1429+
})
1430+
);
1431+
});
13221432
});
13231433

13241434
describe('showing a confirm message when leaving', () => {

x-pack/plugins/lens/public/app_plugin/app.tsx

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import './app.scss';
88

99
import _ from 'lodash';
10-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
10+
import React, { useState, useEffect, useCallback } from 'react';
1111
import { i18n } from '@kbn/i18n';
1212
import { NotificationsStart } from 'kibana/public';
1313
import { EuiBreadcrumb } from '@elastic/eui';
@@ -39,6 +39,7 @@ import {
3939
LensByReferenceInput,
4040
LensEmbeddableInput,
4141
} from '../editor_frame_service/embeddable/embeddable';
42+
import { useTimeRange } from './time_range';
4243

4344
export function App({
4445
history,
@@ -107,43 +108,11 @@ export function App({
107108
state.searchSessionId,
108109
]);
109110

110-
const timefilter = data.query.timefilter.timefilter;
111-
const currentNow = data.nowProvider.get();
112-
113-
// Need a stable reference for the frame component of the dateRange
114-
const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime();
115-
const currentDateRange = useMemo(() => {
116-
const { min, max } = timefilter.calculateBounds({
117-
from: fromDate,
118-
to: toDate,
119-
});
120-
return { fromDate: min?.toISOString() || fromDate, toDate: max?.toISOString() || toDate };
121-
// recalculate current date range if current "now" value changes because calculateBounds
122-
// depends on it internally
123-
// eslint-disable-next-line react-hooks/exhaustive-deps
124-
}, [fromDate, toDate, timefilter, currentNow]);
125-
126-
useEffect(() => {
127-
// TODO check whether dynamic dates are even used - if not, all of this is irrelevant
128-
// calculate length of currently configured range in ms
129-
const timeRangeLength =
130-
new Date(currentDateRange.toDate!).valueOf() - new Date(currentDateRange.fromDate!).valueOf();
131-
// calculate lag of managed "now" for date math
132-
const nowDiff = Date.now() - data.nowProvider.get().valueOf();
133-
// if the lag is signifcant, start a new session to clear the cache
134-
if (nowDiff > timeRangeLength * 0.02) {
135-
setState((s) => ({
136-
...s,
137-
searchSessionId: data.search.session.start(),
138-
}));
139-
}
140-
}, [
141-
currentDateRange.fromDate,
142-
currentDateRange.toDate,
143-
data.nowProvider,
144-
data.search.session,
111+
const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange(
112+
data,
145113
state.lastKnownDoc,
146-
]);
114+
setState
115+
);
147116

148117
const onError = useCallback(
149118
(e: { message: string }) =>
@@ -692,7 +661,7 @@ export function App({
692661
render={editorFrame.mount}
693662
nativeProps={{
694663
searchSessionId: state.searchSessionId,
695-
dateRange: currentDateRange,
664+
dateRange: resolvedDateRange,
696665
query: state.query,
697666
filters: state.filters,
698667
savedQuery: state.savedQuery,

0 commit comments

Comments
 (0)