Skip to content

Commit

Permalink
refactor logic into custom hook and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 committed Jan 18, 2021
1 parent 9e08540 commit 11e85c4
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 64 deletions.
162 changes: 136 additions & 26 deletions x-pack/plugins/lens/public/app_plugin/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { App } from './app';
import { LensAppProps, LensAppServices } from './types';
import { EditorFrameInstance } from '../types';
import { EditorFrameInstance, EditorFrameProps } from '../types';
import { Document } from '../persistence';
import { DOC_TYPE } from '../../common';
import { mount } from 'enzyme';
Expand Down Expand Up @@ -44,6 +44,8 @@ import {
import { LensAttributeService } from '../lens_attribute_service';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public';
import { NativeRenderer } from '../native_renderer';
import moment from 'moment';

jest.mock('../editor_frame_service/editor_frame/expression_helpers');
jest.mock('src/core/public');
Expand Down Expand Up @@ -144,6 +146,11 @@ function createMockTimefilter() {
return unsubscribe;
},
}),
calculateBounds: jest.fn(() => ({
min: moment('2021-01-10T04:00:00.000Z'),
max: moment('2021-01-10T08:00:00.000Z'),
})),
getBounds: jest.fn(() => timeFilter),
getRefreshInterval: () => {},
getRefreshIntervalDefaults: () => {},
getAutoRefreshFetch$: () => ({
Expand Down Expand Up @@ -233,6 +240,9 @@ describe('Lens App', () => {
}),
},
search: createMockSearchService(),
nowProvider: {
get: jest.fn(),
},
} as unknown) as DataPublicPluginStart,
storage: {
get: jest.fn(),
Expand Down Expand Up @@ -306,8 +316,8 @@ describe('Lens App', () => {
/>,
Object {
"dateRange": Object {
"fromDate": "now-7d",
"toDate": "now",
"fromDate": "2021-01-10T04:00:00.000Z",
"toDate": "2021-01-10T08:00:00.000Z",
},
"doc": undefined,
"filters": Array [],
Expand Down Expand Up @@ -350,7 +360,7 @@ describe('Lens App', () => {
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
dateRange: { fromDate: 'now-7d', toDate: 'now' },
dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
query: { query: '', language: 'kuery' },
filters: [pinnedFilter],
})
Expand Down Expand Up @@ -1008,7 +1018,7 @@ describe('Lens App', () => {
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
dateRange: { fromDate: 'now-7d', toDate: 'now' },
dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' },
query: { query: '', language: 'kuery' },
})
);
Expand Down Expand Up @@ -1055,7 +1065,11 @@ describe('Lens App', () => {
});

it('updates the editor frame when the user changes query or time in the search bar', () => {
const { component, frame } = mountWith({});
const { component, frame, services } = mountWith({});
(services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({
min: moment('2021-01-09T04:00:00.000Z'),
max: moment('2021-01-09T08:00:00.000Z'),
});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
Expand All @@ -1071,10 +1085,14 @@ describe('Lens App', () => {
}),
{}
);
expect(services.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({
from: 'now-14d',
to: 'now-7d',
});
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
dateRange: { fromDate: 'now-14d', toDate: 'now-7d' },
dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' },
query: { query: 'new', language: 'lucene' },
})
);
Expand Down Expand Up @@ -1237,6 +1255,34 @@ describe('Lens App', () => {
);
});

it('clears all existing unpinned filters when the active saved query is cleared', () => {
const { component, frame, services } = mountWith({});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
query: { query: 'new', language: 'lucene' },
})
);
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
const field = ({ name: 'myfield' } as unknown) as IFieldType;
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
component.update();
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
component.update();
expect(frame.mount).toHaveBeenLastCalledWith(
expect.any(Element),
expect.objectContaining({
filters: [pinned],
})
);
});
});

describe('search session id management', () => {
it('updates the searchSessionId when the query is updated', () => {
const { component, frame } = mountWith({});
act(() => {
Expand All @@ -1263,12 +1309,12 @@ describe('Lens App', () => {
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-1`,
searchSessionId: `sessionId-2`,
})
);
});

it('clears all existing unpinned filters when the active saved query is cleared', () => {
it('updates the searchSessionId when the active saved query is cleared', () => {
const { component, frame, services } = mountWith({});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
Expand All @@ -1286,31 +1332,67 @@ describe('Lens App', () => {
component.update();
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
component.update();
expect(frame.mount).toHaveBeenLastCalledWith(
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
filters: [pinned],
searchSessionId: `sessionId-2`,
})
);
});

it('updates the searchSessionId when the active saved query is cleared', () => {
const { component, frame, services } = mountWith({});
act(() =>
component.find(TopNavMenu).prop('onQuerySubmit')!({
dateRange: { from: 'now-14d', to: 'now-7d' },
query: { query: 'new', language: 'lucene' },
const mockUpdate = {
filterableIndexPatterns: [],
doc: {
title: '',
description: '',
visualizationType: '',
state: {
datasourceStates: {},
visualization: {},
filters: [],
query: { query: '', language: 'lucene' },
},
references: [],
},
isSaveable: true,
activeData: undefined,
};

it('does not update the searchSessionId when the state changes', () => {
const { component, frame } = mountWith({});
act(() => {
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
mockUpdate
);
});
component.update();
expect(frame.mount).not.toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-2`,
})
);
const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern;
const field = ({ name: 'myfield' } as unknown) as IFieldType;
const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType;
const unpinned = esFilters.buildExistsFilter(field, indexPattern);
const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern);
FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE);
act(() => services.data.query.filterManager.setFilters([pinned, unpinned]));
component.update();
act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!());
});

it('does update the searchSessionId when the state changes and too much time passed', () => {
const { component, frame, services } = mountWith({});

// time range is 100,000ms ago to 30,000ms ago (that's a lag of 30 percent)
(services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000));
(services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
from: 'now-2m',
to: 'now',
});
(services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
min: moment(Date.now() - 100000),
max: moment(Date.now() - 30000),
});

act(() => {
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
mockUpdate
);
});
component.update();
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
Expand All @@ -1319,6 +1401,34 @@ describe('Lens App', () => {
})
);
});

it('does not update the searchSessionId when the state changes and too little time has passed', () => {
const { component, frame, services } = mountWith({});

// time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update)
(services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300));
(services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({
from: 'now-2m',
to: 'now',
});
(services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({
min: moment(Date.now() - 100000),
max: moment(Date.now() - 300),
});

act(() => {
(component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange(
mockUpdate
);
});
component.update();
expect(frame.mount).not.toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-2`,
})
);
});
});

describe('showing a confirm message when leaving', () => {
Expand Down
45 changes: 7 additions & 38 deletions x-pack/plugins/lens/public/app_plugin/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import './app.scss';

import _ from 'lodash';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { EuiBreadcrumb } from '@elastic/eui';
Expand Down Expand Up @@ -39,6 +39,7 @@ import {
LensByReferenceInput,
LensEmbeddableInput,
} from '../editor_frame_service/embeddable/embeddable';
import { useTimeRange } from './time_range';

export function App({
history,
Expand Down Expand Up @@ -107,43 +108,11 @@ export function App({
state.searchSessionId,
]);

const timefilter = data.query.timefilter.timefilter;
const currentNow = data.nowProvider.get();

// Need a stable reference for the frame component of the dateRange
const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime();
const currentDateRange = useMemo(() => {
const { min, max } = timefilter.calculateBounds({
from: fromDate,
to: toDate,
});
return { fromDate: min?.toISOString() || fromDate, toDate: max?.toISOString() || toDate };
// recalculate current date range if current "now" value changes because calculateBounds
// depends on it internally
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fromDate, toDate, timefilter, currentNow]);

useEffect(() => {
// TODO check whether dynamic dates are even used - if not, all of this is irrelevant
// calculate length of currently configured range in ms
const timeRangeLength =
new Date(currentDateRange.toDate!).valueOf() - new Date(currentDateRange.fromDate!).valueOf();
// calculate lag of managed "now" for date math
const nowDiff = Date.now() - data.nowProvider.get().valueOf();
// if the lag is signifcant, start a new session to clear the cache
if (nowDiff > timeRangeLength * 0.02) {
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
}
}, [
currentDateRange.fromDate,
currentDateRange.toDate,
data.nowProvider,
data.search.session,
const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange(
data,
state.lastKnownDoc,
]);
setState
);

const onError = useCallback(
(e: { message: string }) =>
Expand Down Expand Up @@ -692,7 +661,7 @@ export function App({
render={editorFrame.mount}
nativeProps={{
searchSessionId: state.searchSessionId,
dateRange: currentDateRange,
dateRange: resolvedDateRange,
query: state.query,
filters: state.filters,
savedQuery: state.savedQuery,
Expand Down
Loading

0 comments on commit 11e85c4

Please sign in to comment.