[8.x] [Discover][ES]QL] Ensure the same time range is being used for documents and histogram (#204694) (#206563)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Discover][ES]QL] Ensure the same time range is being used for
documents and histogram
(#204694)](https://github.com/elastic/kibana/pull/204694)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Matthias
Wilhelm","email":"matthias.wilhelm@elastic.co"},"sourceCommit":{"committedDate":"2025-01-10T16:06:45Z","message":"[Discover][ES]QL]
Ensure the same time range is being used for documents and histogram
(#204694)\n\nFixing 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.\n\nFixes a redundant ES|QL
request for histogram data, with different timeranges, when the
timerange is
changed.","sha":"df495ce532e4b478984830d533dac238bfd15444","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Discover","release_note:fix","backport
missing","v9.0.0","Team:DataDiscovery","backport:prev-minor","Feature:ES|QL"],"number":204694,"url":"https://github.com/elastic/kibana/pull/204694","mergeCommit":{"message":"[Discover][ES]QL]
Ensure the same time range is being used for documents and histogram
(#204694)\n\nFixing 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.\n\nFixes a redundant ES|QL
request for histogram data, with different timeranges, when the
timerange is
changed.","sha":"df495ce532e4b478984830d533dac238bfd15444"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204694","number":204694,"mergeCommit":{"message":"[Discover][ES]QL]
Ensure the same time range is being used for documents and histogram
(#204694)\n\nFixing 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.\n\nFixes a redundant ES|QL
request for histogram data, with different timeranges, when the
timerange is
changed.","sha":"df495ce532e4b478984830d533dac238bfd15444"}}]}]
BACKPORT-->
This commit is contained in:
Matthias Wilhelm 2025-01-14 19:23:35 +01:00 committed by GitHub
parent bf9c61bb56
commit 3231af94c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 160 additions and 69 deletions

View file

@ -45,6 +45,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 = {

View file

@ -49,7 +49,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';
@ -118,6 +117,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,
@ -283,20 +283,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(

View file

@ -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 } } }))
);

View file

@ -104,6 +104,10 @@ async function mountComponent(
query,
});
stateContainer.internalState.transitions.setDataView(dataView);
stateContainer.internalState.transitions.setDataRequestParams({
timeRangeAbsolute: time,
timeRangeRelative: time,
});
const props = {
dataView,

View file

@ -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', () => {

View file

@ -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

View file

@ -81,6 +81,7 @@ describe('test fetchAll', () => {
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}),
searchSessionId: '123',
initialFetchStatus: FetchStatus.UNINITIALIZED,
@ -275,6 +276,7 @@ describe('test fetchAll', () => {
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}),
};
fetchAll(subjects, false, deps);
@ -399,6 +401,7 @@ describe('test fetchAll', () => {
rowHeight: false,
breakdownField: false,
},
dataRequestParams: {},
}),
};
fetchAll(subjects, false, deps);

View file

@ -98,6 +98,7 @@ export function fetchAll(
services,
sort: getAppState().sort as SortOrder[],
customFilters: getInternalState().customFilters,
inputTimeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
});
}
@ -118,6 +119,7 @@ export function fetchAll(
data,
expressions,
profilesManager,
timeRange: getInternalState().dataRequestParams.timeRangeAbsolute,
})
: fetchDocuments(searchSource, fetchDeps);
const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';

View file

@ -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);
});
});

View file

@ -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.',
}),
};
}

View file

@ -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 { SEARCH_FIELDS_FROM_SOURCE, 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;
@ -49,7 +51,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;
}

View file

@ -259,6 +259,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'),

View file

@ -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'>) => ({

View file

@ -151,7 +151,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,

View file

@ -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 () => {