mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
0a7e498bac
commit
5cd8b0431b
4 changed files with 235 additions and 41 deletions
|
@ -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', () => {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
82
x-pack/plugins/lens/public/app_plugin/time_range.ts
Normal file
82
x-pack/plugins/lens/public/app_plugin/time_range.ts
Normal 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 };
|
||||
}
|
|
@ -252,7 +252,6 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
state.visualization,
|
||||
state.activeData,
|
||||
props.query,
|
||||
props.dateRange,
|
||||
props.filters,
|
||||
props.savedQuery,
|
||||
state.title,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue