mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Discover][ES]QL] Ensure the same time range is being used for documents and histogram (#204694)
Fixing the case a relative time range is set (for example "Last 15 minutes") and the time range goes out of sync for table and histogram requests on Discover in ES|QL mode. Fixes a redundant ES|QL request for histogram data, with different timeranges, when the timerange is changed.
This commit is contained in:
parent
b37ec3ce20
commit
df495ce532
15 changed files with 160 additions and 69 deletions
|
@ -46,6 +46,17 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
|
|||
stateContainer.appState.update({
|
||||
dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
|
||||
});
|
||||
stateContainer.internalState.transitions.setDataRequestParams({
|
||||
timeRangeRelative: {
|
||||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
},
|
||||
timeRangeAbsolute: {
|
||||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
},
|
||||
});
|
||||
|
||||
stateContainer.dataState.data$.documents$ = documents$;
|
||||
|
||||
const props = {
|
||||
|
|
|
@ -46,7 +46,6 @@ import useObservable from 'react-use/lib/useObservable';
|
|||
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
|
||||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||
import { map } from 'rxjs';
|
||||
import { DiscoverGrid } from '../../../../components/discover_grid';
|
||||
import { getDefaultRowsPerPage } from '../../../../../common/constants';
|
||||
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
|
||||
|
@ -112,6 +111,7 @@ function DiscoverDocumentsComponent({
|
|||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const { dataViews, capabilities, uiSettings, uiActions, ebtManager, fieldsMetadata } = services;
|
||||
const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
|
||||
const [
|
||||
dataSource,
|
||||
query,
|
||||
|
@ -269,20 +269,14 @@ function DiscoverDocumentsComponent({
|
|||
: undefined,
|
||||
[documentState.esqlQueryColumns]
|
||||
);
|
||||
|
||||
const { filters } = useQuerySubscriber({ data: services.data });
|
||||
|
||||
const timeRange = useObservable(
|
||||
services.timefilter.getTimeUpdate$().pipe(map(() => services.timefilter.getTime())),
|
||||
services.timefilter.getTime()
|
||||
);
|
||||
|
||||
const cellActionsMetadata = useAdditionalCellActions({
|
||||
dataSource,
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
timeRange: requestParams.timeRangeAbsolute,
|
||||
});
|
||||
|
||||
const renderDocumentView = useCallback(
|
||||
|
|
|
@ -38,14 +38,25 @@ import { createDataViewDataSource } from '../../../../../common/data_sources';
|
|||
function getStateContainer(savedSearch?: SavedSearch) {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true, savedSearch });
|
||||
const dataView = savedSearch?.searchSource?.getField('index') as DataView;
|
||||
|
||||
stateContainer.appState.update({
|
||||
const appState = {
|
||||
dataSource: createDataViewDataSource({ dataViewId: dataView?.id! }),
|
||||
interval: 'auto',
|
||||
hideChart: false,
|
||||
});
|
||||
};
|
||||
|
||||
stateContainer.appState.update(appState);
|
||||
|
||||
stateContainer.internalState.transitions.setDataView(dataView);
|
||||
stateContainer.internalState.transitions.setDataRequestParams({
|
||||
timeRangeAbsolute: {
|
||||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
},
|
||||
timeRangeRelative: {
|
||||
from: '2020-05-14T11:05:13.590',
|
||||
to: '2020-05-14T11:20:13.590',
|
||||
},
|
||||
});
|
||||
|
||||
return stateContainer;
|
||||
}
|
||||
|
@ -65,16 +76,7 @@ const mountComponent = async ({
|
|||
const dataView = savedSearch?.searchSource?.getField('index') as DataView;
|
||||
|
||||
let services = discoverServiceMock;
|
||||
services.data.query.timefilter.timefilter.getAbsoluteTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
};
|
||||
services.data.query.timefilter.timefilter.getTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
};
|
||||
(services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
});
|
||||
|
||||
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
|
||||
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } }))
|
||||
);
|
||||
|
|
|
@ -105,6 +105,10 @@ async function mountComponent(
|
|||
query,
|
||||
});
|
||||
stateContainer.internalState.transitions.setDataView(dataView);
|
||||
stateContainer.internalState.transitions.setDataRequestParams({
|
||||
timeRangeAbsolute: time,
|
||||
timeRangeRelative: time,
|
||||
});
|
||||
|
||||
const props = {
|
||||
dataView,
|
||||
|
|
|
@ -379,6 +379,23 @@ describe('useDiscoverHistogram', () => {
|
|||
});
|
||||
expect(hook.result.current.isChartLoading).toBe(true);
|
||||
});
|
||||
|
||||
it('should use timerange + timeRangeRelative + query given by the internalState container', async () => {
|
||||
const fetch$ = new Subject<void>();
|
||||
const stateContainer = getStateContainer();
|
||||
const timeRangeAbs = { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
|
||||
const timeRangeRel = { from: 'now-15m', to: 'now' };
|
||||
stateContainer.internalState.transitions.setDataRequestParams({
|
||||
timeRangeAbsolute: timeRangeAbs,
|
||||
timeRangeRelative: timeRangeRel,
|
||||
});
|
||||
const { hook } = await renderUseDiscoverHistogram({ stateContainer });
|
||||
act(() => {
|
||||
fetch$.next();
|
||||
});
|
||||
expect(hook.result.current.timeRange).toBe(timeRangeAbs);
|
||||
expect(hook.result.current.relativeTimeRange).toBe(timeRangeRel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refetching', () => {
|
||||
|
|
|
@ -217,14 +217,9 @@ export const useDiscoverHistogram = ({
|
|||
* Request params
|
||||
*/
|
||||
const { query, filters } = useQuerySubscriber({ data: services.data });
|
||||
const requestParams = useInternalStateSelector((state) => state.dataRequestParams);
|
||||
const customFilters = useInternalStateSelector((state) => state.customFilters);
|
||||
const timefilter = services.data.query.timefilter.timefilter;
|
||||
const timeRange = timefilter.getAbsoluteTime();
|
||||
const relativeTimeRange = useObservable(
|
||||
timefilter.getTimeUpdate$().pipe(map(() => timefilter.getTime())),
|
||||
timefilter.getTime()
|
||||
);
|
||||
|
||||
const { timeRangeRelative: relativeTimeRange, timeRangeAbsolute: timeRange } = requestParams;
|
||||
// When in ES|QL mode, update the data view, query, and
|
||||
// columns only when documents are done fetching so the Lens suggestions
|
||||
// don't frequently change, such as when the user modifies the table
|
||||
|
|
|
@ -81,6 +81,7 @@ describe('test fetchAll', () => {
|
|||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
dataRequestParams: {},
|
||||
}),
|
||||
searchSessionId: '123',
|
||||
initialFetchStatus: FetchStatus.UNINITIALIZED,
|
||||
|
@ -273,6 +274,7 @@ describe('test fetchAll', () => {
|
|||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
dataRequestParams: {},
|
||||
}),
|
||||
};
|
||||
fetchAll(subjects, false, deps);
|
||||
|
@ -396,6 +398,7 @@ describe('test fetchAll', () => {
|
|||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
dataRequestParams: {},
|
||||
}),
|
||||
};
|
||||
fetchAll(subjects, false, deps);
|
||||
|
|
|
@ -97,6 +97,7 @@ export function fetchAll(
|
|||
services,
|
||||
sort: getAppState().sort as SortOrder[],
|
||||
customFilters: getInternalState().customFilters,
|
||||
inputTimeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -117,6 +118,7 @@ export function fetchAll(
|
|||
data,
|
||||
expressions,
|
||||
profilesManager,
|
||||
timeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
|
||||
})
|
||||
: fetchDocuments(searchSource, fetchDeps);
|
||||
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
|
||||
|
|
|
@ -13,13 +13,23 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
|||
import { of } from 'rxjs';
|
||||
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
|
||||
import { discoverServiceMock } from '../../../__mocks__/services';
|
||||
import { fetchEsql } from './fetch_esql';
|
||||
import { fetchEsql, getTextBasedQueryStateToAstProps } from './fetch_esql';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
|
||||
describe('fetchEsql', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const fetchEsqlMockProps = {
|
||||
query: { esql: 'from *' },
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
inspectorAdapters: { requests: new RequestAdapter() },
|
||||
data: discoverServiceMock.data,
|
||||
expressions: discoverServiceMock.expressions,
|
||||
profilesManager: discoverServiceMock.profilesManager,
|
||||
};
|
||||
|
||||
it('resolves with returned records', async () => {
|
||||
const hits = [
|
||||
{ _id: '1', foo: 'bar' },
|
||||
|
@ -46,16 +56,7 @@ describe('fetchEsql', () => {
|
|||
discoverServiceMock.profilesManager,
|
||||
'resolveDocumentProfile'
|
||||
);
|
||||
expect(
|
||||
await fetchEsql({
|
||||
query: { esql: 'from *' },
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
inspectorAdapters: { requests: new RequestAdapter() },
|
||||
data: discoverServiceMock.data,
|
||||
expressions: discoverServiceMock.expressions,
|
||||
profilesManager: discoverServiceMock.profilesManager,
|
||||
})
|
||||
).toEqual({
|
||||
expect(await fetchEsql(fetchEsqlMockProps)).toEqual({
|
||||
records,
|
||||
esqlQueryColumns: ['_id', 'foo'],
|
||||
esqlHeaderWarning: undefined,
|
||||
|
@ -64,4 +65,24 @@ describe('fetchEsql', () => {
|
|||
expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[0] });
|
||||
expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[1] });
|
||||
});
|
||||
|
||||
it('should use inputTimeRange if provided', () => {
|
||||
const timeRange: TimeRange = { from: 'now-15m', to: 'now' };
|
||||
const result = getTextBasedQueryStateToAstProps({ ...fetchEsqlMockProps, timeRange });
|
||||
expect(result.time).toEqual(timeRange);
|
||||
});
|
||||
|
||||
it('should use absolute time from data if inputTimeRange is not provided', () => {
|
||||
const absoluteTimeRange: TimeRange = {
|
||||
from: '2021-08-31T22:00:00.000Z',
|
||||
to: '2021-09-01T22:00:00.000Z',
|
||||
};
|
||||
jest
|
||||
.spyOn(discoverServiceMock.data.query.timefilter.timefilter, 'getAbsoluteTime')
|
||||
.mockReturnValue(absoluteTimeRange);
|
||||
|
||||
const result = getTextBasedQueryStateToAstProps(fetchEsqlMockProps);
|
||||
|
||||
expect(result.time).toEqual(absoluteTimeRange);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@ export function fetchEsql({
|
|||
query,
|
||||
inputQuery,
|
||||
filters,
|
||||
inputTimeRange,
|
||||
timeRange,
|
||||
dataView,
|
||||
abortSignal,
|
||||
inspectorAdapters,
|
||||
|
@ -43,7 +43,7 @@ export function fetchEsql({
|
|||
query: Query | AggregateQuery;
|
||||
inputQuery?: Query;
|
||||
filters?: Filter[];
|
||||
inputTimeRange?: TimeRange;
|
||||
timeRange?: TimeRange;
|
||||
dataView: DataView;
|
||||
abortSignal?: AbortSignal;
|
||||
inspectorAdapters: Adapters;
|
||||
|
@ -51,20 +51,15 @@ export function fetchEsql({
|
|||
expressions: ExpressionsStart;
|
||||
profilesManager: ProfilesManager;
|
||||
}): Promise<RecordsFetchResponse> {
|
||||
const timeRange = inputTimeRange ?? data.query.timefilter.timefilter.getTime();
|
||||
return textBasedQueryStateToAstWithValidation({
|
||||
filters,
|
||||
const props = getTextBasedQueryStateToAstProps({
|
||||
query,
|
||||
time: timeRange,
|
||||
timeFieldName: dataView.timeFieldName,
|
||||
inputQuery,
|
||||
titleForInspector: i18n.translate('discover.inspectorEsqlRequestTitle', {
|
||||
defaultMessage: 'Table',
|
||||
}),
|
||||
descriptionForInspector: i18n.translate('discover.inspectorEsqlRequestDescription', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch results for the table.',
|
||||
}),
|
||||
})
|
||||
filters,
|
||||
timeRange,
|
||||
dataView,
|
||||
data,
|
||||
});
|
||||
return textBasedQueryStateToAstWithValidation(props)
|
||||
.then((ast) => {
|
||||
if (ast) {
|
||||
const contract = expressions.execute(ast, null, {
|
||||
|
@ -118,3 +113,32 @@ export function fetchEsql({
|
|||
throw new Error(err.message);
|
||||
});
|
||||
}
|
||||
export function getTextBasedQueryStateToAstProps({
|
||||
query,
|
||||
inputQuery,
|
||||
filters,
|
||||
timeRange,
|
||||
dataView,
|
||||
data,
|
||||
}: {
|
||||
query: Query | AggregateQuery;
|
||||
inputQuery?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
dataView: DataView;
|
||||
data: DataPublicPluginStart;
|
||||
}) {
|
||||
return {
|
||||
filters,
|
||||
query,
|
||||
time: timeRange ?? data.query.timefilter.timefilter.getAbsoluteTime(),
|
||||
timeFieldName: dataView.timeFieldName,
|
||||
inputQuery,
|
||||
titleForInspector: i18n.translate('discover.inspectorEsqlRequestTitle', {
|
||||
defaultMessage: 'Table',
|
||||
}),
|
||||
descriptionForInspector: i18n.translate('discover.inspectorEsqlRequestDescription', {
|
||||
defaultMessage: 'This request queries Elasticsearch to fetch results for the table.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ISearchSource } from '@kbn/data-plugin/public';
|
||||
import { DataViewType, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import type { ISearchSource } from '@kbn/data-plugin/public';
|
||||
import { DataViewType, type DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { Filter, TimeRange } from '@kbn/es-query';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
|
@ -25,11 +25,13 @@ export function updateVolatileSearchSource(
|
|||
services,
|
||||
sort,
|
||||
customFilters,
|
||||
inputTimeRange,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
services: DiscoverServices;
|
||||
sort?: SortOrder[];
|
||||
customFilters: Filter[];
|
||||
inputTimeRange?: TimeRange;
|
||||
}
|
||||
) {
|
||||
const { uiSettings, data } = services;
|
||||
|
@ -48,7 +50,7 @@ export function updateVolatileSearchSource(
|
|||
|
||||
if (dataView.type !== DataViewType.ROLLUP) {
|
||||
// Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range
|
||||
const timeFilter = data.query.timefilter.timefilter.createFilter(dataView);
|
||||
const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, inputTimeRange);
|
||||
filters = timeFilter ? [...filters, timeFilter] : filters;
|
||||
}
|
||||
|
||||
|
|
|
@ -254,6 +254,11 @@ export function getDataStateContainer({
|
|||
return;
|
||||
}
|
||||
|
||||
internalStateContainer.transitions.setDataRequestParams({
|
||||
timeRangeAbsolute: timefilter.getAbsoluteTime(),
|
||||
timeRangeRelative: timefilter.getTime(),
|
||||
});
|
||||
|
||||
await profilesManager.resolveDataSourceProfile({
|
||||
dataSource: appStateContainer.getState().dataSource,
|
||||
dataView: getSavedSearch().searchSource.getField('index'),
|
||||
|
|
|
@ -14,10 +14,15 @@ import {
|
|||
ReduxLikeStateContainer,
|
||||
} from '@kbn/kibana-utils-plugin/common';
|
||||
import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { Filter, TimeRange } from '@kbn/es-query';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public';
|
||||
|
||||
interface InternalStateDataRequestParams {
|
||||
timeRangeAbsolute?: TimeRange;
|
||||
timeRangeRelative?: TimeRange;
|
||||
}
|
||||
|
||||
export interface InternalState {
|
||||
dataView: DataView | undefined;
|
||||
isDataViewLoading: boolean;
|
||||
|
@ -33,6 +38,7 @@ export interface InternalState {
|
|||
rowHeight: boolean;
|
||||
breakdownField: boolean;
|
||||
};
|
||||
dataRequestParams: InternalStateDataRequestParams;
|
||||
}
|
||||
|
||||
export interface InternalStateTransitions {
|
||||
|
@ -65,6 +71,9 @@ export interface InternalStateTransitions {
|
|||
) => (
|
||||
resetDefaultProfileState: Omit<InternalState['resetDefaultProfileState'], 'resetId'>
|
||||
) => InternalState;
|
||||
setDataRequestParams: (
|
||||
state: InternalState
|
||||
) => (params: InternalStateDataRequestParams) => InternalState;
|
||||
}
|
||||
|
||||
export type DiscoverInternalStateContainer = ReduxLikeStateContainer<
|
||||
|
@ -91,6 +100,7 @@ export function getInternalStateContainer() {
|
|||
rowHeight: false,
|
||||
breakdownField: false,
|
||||
},
|
||||
dataRequestParams: {},
|
||||
},
|
||||
{
|
||||
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
|
||||
|
@ -162,6 +172,11 @@ export function getInternalStateContainer() {
|
|||
overriddenVisContextAfterInvalidation: undefined,
|
||||
expandedDoc: undefined,
|
||||
}),
|
||||
setDataRequestParams:
|
||||
(prevState: InternalState) => (params: InternalStateDataRequestParams) => ({
|
||||
...prevState,
|
||||
dataRequestParams: params,
|
||||
}),
|
||||
setResetDefaultProfileState:
|
||||
(prevState: InternalState) =>
|
||||
(resetDefaultProfileState: Omit<InternalState['resetDefaultProfileState'], 'resetId'>) => ({
|
||||
|
|
|
@ -148,7 +148,7 @@ export function initializeFetch({
|
|||
// Request ES|QL data
|
||||
const result = await fetchEsql({
|
||||
query: searchSourceQuery,
|
||||
inputTimeRange: getTimeRangeFromFetchContext(fetchContext),
|
||||
timeRange: getTimeRangeFromFetchContext(fetchContext),
|
||||
inputQuery: fetchContext.query,
|
||||
filters: fetchContext.filters,
|
||||
dataView,
|
||||
|
|
|
@ -138,16 +138,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the time range`, async () => {
|
||||
await expectSearches(
|
||||
type,
|
||||
type === 'esql' ? expectedRequests + 1 : expectedRequests,
|
||||
async () => {
|
||||
await timePicker.setAbsoluteRange(
|
||||
'Sep 21, 2015 @ 06:31:44.000',
|
||||
'Sep 23, 2015 @ 00:00:00.000'
|
||||
);
|
||||
}
|
||||
);
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
await timePicker.setAbsoluteRange(
|
||||
'Sep 21, 2015 @ 06:31:44.000',
|
||||
'Sep 23, 2015 @ 00:00:00.000'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send ${savedSearchesRequests} requests for saved search changes`, async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue