[Lens] Restart session if fixed now becomes outdated (#88575) (#88942)

This commit is contained in:
Joe Reuter 2021-01-21 14:05:03 +01:00 committed by GitHub
parent 0a7e498bac
commit 5cd8b0431b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 235 additions and 41 deletions

View file

@ -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';
@ -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');
@ -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$: () => ({
@ -233,6 +240,9 @@ describe('Lens App', () => {
}),
},
search: createMockSearchService(),
nowProvider: {
get: jest.fn(),
},
} as unknown) as DataPublicPluginStart,
storage: {
get: jest.fn(),
@ -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 [],
@ -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],
})
@ -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' },
})
);
@ -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' },
@ -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' },
})
);
@ -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(() => {
@ -1263,33 +1309,7 @@ describe('Lens App', () => {
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
searchSessionId: `sessionId-1`,
})
);
});
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],
searchSessionId: `sessionId-2`,
})
);
});
@ -1319,6 +1339,96 @@ describe('Lens App', () => {
})
);
});
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`,
})
);
});
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),
expect.objectContaining({
searchSessionId: `sessionId-2`,
})
);
});
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', () => {

View file

@ -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';
@ -39,6 +39,7 @@ import {
LensByReferenceInput,
LensEmbeddableInput,
} from '../editor_frame_service/embeddable/embeddable';
import { useTimeRange } from './time_range';
export function App({
history,
@ -107,9 +108,11 @@ export function App({
state.searchSessionId,
]);
// Need a stable reference for the frame component of the dateRange
const { from: fromDate, to: toDate } = data.query.timefilter.timefilter.getTime();
const currentDateRange = useMemo(() => ({ fromDate, toDate }), [fromDate, toDate]);
const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange(
data,
state.lastKnownDoc,
setState
);
const onError = useCallback(
(e: { message: string }) =>
@ -658,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,
@ -670,7 +673,7 @@ export function App({
if (isSaveable !== state.isSaveable) {
setState((s) => ({ ...s, isSaveable }));
}
if (!_.isEqual(state.persistedDoc, doc)) {
if (!_.isEqual(state.persistedDoc, doc) && !_.isEqual(state.lastKnownDoc, doc)) {
setState((s) => ({ ...s, lastKnownDoc: doc }));
}
if (!_.isEqual(state.activeData, activeData)) {

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './app.scss';
import _ from 'lodash';
import moment from 'moment';
import { useEffect, useMemo } from 'react';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { LensAppState } from './types';
import { Document } from '../persistence';
function containsDynamicMath(dateMathString: string) {
return dateMathString.includes('now');
}
const TIME_LAG_PERCENTAGE_LIMIT = 0.02;
/**
* Fetches the current global time range from data plugin and restarts session
* if the fixed "now" parameter is diverging too much from the actual current time.
* @param data data plugin contract to manage current now value, time range and session
* @param lastKnownDoc Current state of the editor
* @param setState state setter for Lens app state
*/
export function useTimeRange(
data: DataPublicPluginStart,
lastKnownDoc: Document | undefined,
setState: React.Dispatch<React.SetStateAction<LensAppState>>
) {
const timefilter = data.query.timefilter.timefilter;
const { from, to } = data.query.timefilter.timefilter.getTime();
const currentNow = data.nowProvider.get();
// Need a stable reference for the frame component of the dateRange
const resolvedDateRange = useMemo(() => {
const { min, max } = timefilter.calculateBounds({
from,
to,
});
return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to };
// recalculate current date range if current "now" value changes because calculateBounds
// depends on it internally
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timefilter, currentNow, from, to]);
useEffect(() => {
const unresolvedTimeRange = timefilter.getTime();
if (
!containsDynamicMath(unresolvedTimeRange.from) &&
!containsDynamicMath(unresolvedTimeRange.to)
) {
return;
}
const { min, max } = timefilter.getBounds();
if (!min || !max) {
// bounds not fully specified, bailing out
return;
}
// calculate length of currently configured range in ms
const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds();
// 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 * TIME_LAG_PERCENTAGE_LIMIT) {
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
}
}, [data.nowProvider, data.search.session, timefilter, lastKnownDoc, setState]);
return { resolvedDateRange, from, to };
}

View file

@ -252,7 +252,6 @@ export function EditorFrame(props: EditorFrameProps) {
state.visualization,
state.activeData,
props.query,
props.dateRange,
props.filters,
props.savedQuery,
state.title,