diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index 18b360de9aaa64..bdbb381262ccca 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -50,6 +50,13 @@ export function buildEsQuery( config.allowLeadingWildcards, config.dateFormatTZ ); + // TODO there is probably a more elegant way than this. + // We need to pass raw queries to the filters agg somehow + const rawQuery = buildQueryFromFilters( + queriesByLanguage.rawQuery ? (queriesByLanguage.rawQuery.map((q) => q.query) as Filter[]) : [], + indexPattern, + config.ignoreFilterIfFieldNotInIndex + ); const luceneQuery = buildQueryFromLucene( queriesByLanguage.lucene, config.queryStringOptions, @@ -63,10 +70,25 @@ export function buildEsQuery( return { bool: { - must: [...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must], - filter: [...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter], - should: [...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should], - must_not: [...kueryQuery.must_not, ...luceneQuery.must_not, ...filterQuery.must_not], + must: [...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must, ...rawQuery.must], + filter: [ + ...kueryQuery.filter, + ...luceneQuery.filter, + ...filterQuery.filter, + ...rawQuery.filter, + ], + should: [ + ...kueryQuery.should, + ...luceneQuery.should, + ...filterQuery.should, + ...rawQuery.should, + ], + must_not: [ + ...kueryQuery.must_not, + ...luceneQuery.must_not, + ...filterQuery.must_not, + ...rawQuery.must_not, + ], }, }; } diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 0e9cf6aeb1f2f7..ee4e96ffcb46a7 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -17,6 +17,7 @@ import { SerializedFieldFormat, } from 'src/plugins/expressions/common'; +import moment from 'moment'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -172,6 +173,18 @@ export class AggConfig { return _.get(this.params, key); } + getTimeShift(): undefined | moment.Duration { + const rawTimeShift = this.getParam('timeShift'); + if (!rawTimeShift) return undefined; + const [, amount, unit] = rawTimeShift.match(/(\d+)(\w)/); + return moment.duration(Number(amount), unit); + } + + getTimeShiftedFilter(timeShift: moment.Duration, value: any) { + // TODO better handling for implementation vs no implementation + return this.type.getTimeShiftedFilter!(this, timeShift, value); + } + write(aggs?: IAggConfigs) { return writeParams(this.type.params, this, aggs); } diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 33fdc45a605b71..6e8025edfa93d6 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -53,6 +53,7 @@ export interface AggTypeConfig< getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; getValueBucketPath?: (agg: TAggConfig) => string; + getTimeShiftedFilter?: (agg: TAggConfig, timeShift: moment.Duration, value: any) => any; } // TODO need to make a more explicit interface for this @@ -210,6 +211,8 @@ export class AggType< getValue: (agg: TAggConfig, bucket: any) => any; + getTimeShiftedFilter: (agg: TAggConfig, timeShift: moment.Duration, value: any) => any; + getKey?: (bucket: any, key: any, agg: TAggConfig) => any; paramByName = (name: string) => { @@ -283,6 +286,11 @@ export class AggType< this.getResponseAggs = config.getResponseAggs || (() => {}); this.decorateAggConfig = config.decorateAggConfig || (() => ({})); this.postFlightRequest = config.postFlightRequest || identity; + this.getTimeShiftedFilter = + config.getTimeShiftedFilter || + (() => { + throw new Error('not implemented'); + }); this.getSerializedFormat = config.getSerializedFormat || diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index 61ad66d7efdc9a..633ca15965550b 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -133,6 +133,22 @@ export const getDateHistogramBucketAgg = ({ }, }; }, + getTimeShiftedFilter: (agg, timeShift, val) => { + const bucketStart = moment(val).subtract(timeShift); + updateTimeBuckets(agg, calculateBounds); + + const { useNormalizedEsInterval } = agg.params; + const interval = agg.buckets.getInterval(useNormalizedEsInterval); + const bucketEnd = bucketStart.clone().add(interval); + return { + range: { + [agg.fieldName()]: { + gte: bucketStart.toISOString(), + lte: bucketEnd.toISOString(), + }, + }, + }; + }, params: [ { name: 'field', diff --git a/src/plugins/data/common/search/aggs/buckets/filters.ts b/src/plugins/data/common/search/aggs/buckets/filters.ts index 107b86de040587..e8cdceb4bf7bf6 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.ts @@ -23,7 +23,7 @@ const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { 'The name of an aggregation, that allows to specify multiple individual filters to group data by.', }); -interface FilterValue { +export interface FilterValue { input: Query; label: string; id: string; diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 7d37dc83405b8b..2d4ed16c988d10 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -140,6 +140,10 @@ export const getTermsBucketAgg = () => } return resp; }, + getTimeShiftedFilter: (agg, _timeShift, val) => { + // TODO this doesn't work for other/missing buckets + return agg.createFilter(val).query; + }, params: [ { name: 'field', diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index 8a10d7edb3f835..2d2809e6bd4c88 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -25,6 +25,13 @@ export const getCountMetricAgg = () => defaultMessage: 'Count', }); }, + params: [ + { + name: 'timeShift', + type: 'string', + write: () => {}, + }, + ], getSerializedFormat(agg) { return { id: 'number', diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 40c87db57eedca..c1b46f1a332d18 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -48,6 +48,10 @@ export const aggCount = (): FunctionDefinition => ({ defaultMessage: 'Schema to use for this aggregation', }), }, + timeShift: { + types: ['string'], + help: '', + }, customLabel: { types: ['string'], help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index e57410962fc08a..c11d4a6116d92d 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -178,7 +178,7 @@ export interface AggParamsMapping { [BUCKET_TYPES.TERMS]: AggParamsTerms; [METRIC_TYPES.AVG]: AggParamsAvg; [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; - [METRIC_TYPES.COUNT]: BaseAggParams; + [METRIC_TYPES.COUNT]: BaseAggParams & { timeShift?: string }; [METRIC_TYPES.GEO_BOUNDS]: AggParamsGeoBounds; [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; [METRIC_TYPES.MAX]: AggParamsMax; diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index e2ee1a31757cb7..ba74f08dfa2694 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -7,6 +7,8 @@ */ import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { DatatableColumn, DatatableRow } from 'src/plugins/expressions'; import { Adapters } from 'src/plugins/inspector/common'; import { @@ -19,7 +21,7 @@ import { TimeRange, } from '../../../../common'; -import { IAggConfigs } from '../../aggs'; +import { FilterValue, IAggConfigs } from '../../aggs'; import { ISearchStartSearchSource } from '../../search_source'; import { tabifyAggResponse } from '../../tabify'; import { getRequestInspectorStats, getResponseInspectorStats } from '../utils'; @@ -62,126 +64,399 @@ export const handleRequest = async ({ searchSource.setField('index', indexPattern); searchSource.setField('size', 0); - // Create a new search source that inherits the original search source - // but has the appropriate timeRange applied via a filter. - // This is a temporary solution until we properly pass down all required - // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). - // Using callParentStartHandlers: true we make sure, that the parent searchSource - // onSearchRequestStart will be called properly even though we use an inherited - // search source. - const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); - const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); - - aggs.setTimeRange(timeRange as TimeRange); - - // For now we need to mirror the history of the passed search source, since - // the request inspector wouldn't work otherwise. - Object.defineProperty(requestSearchSource, 'history', { - get() { - return searchSource.history; - }, - set(history) { - return (searchSource.history = history); - }, - }); - - requestSearchSource.setField('aggs', function () { - return aggs.toDsl(metricsAtAllLevels); - }); - - requestSearchSource.onRequestStart((paramSearchSource, options) => { - return aggs.onSearchRequestStart(paramSearchSource, options); - }); - - // If timeFields have been specified, use the specified ones, otherwise use primary time field of index - // pattern if it's available. - const defaultTimeField = indexPattern?.getTimeField?.(); - const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; - const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; - - // If a timeRange has been specified and we had at least one timeField available, create range - // filters for that those time fields - if (timeRange && allTimeFields.length > 0) { - timeFilterSearchSource.setField('filter', () => { - return allTimeFields - .map((fieldName) => getTime(indexPattern, timeRange, { fieldName, forceNow })) - .filter(isRangeFilter); + const timeShifts: Record = {}; + aggs + .getAll() + .filter((agg) => agg.schema === 'metric') + .map((agg) => agg.getTimeShift()) + .forEach((timeShift) => { + if (timeShift) { + timeShifts[String(timeShift.asMilliseconds())] = timeShift; + } }); - } - requestSearchSource.setField('filter', filters); - requestSearchSource.setField('query', query); - - let request; - if (inspectorAdapters.requests) { - inspectorAdapters.requests.reset(); - request = inspectorAdapters.requests.start( - i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', - }), - { - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - searchSessionId, - } - ); - request.stats(getRequestInspectorStats(requestSearchSource)); - } + const originalAggs = aggs; - try { - const response = await requestSearchSource.fetch({ - abortSignal, - sessionId: searchSessionId, + const buildBaseResponse = async () => { + const currentAggs = aggs; + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ + callParentStartHandlers: true, }); - if (request) { - request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); - } + const currentTimeRange: TimeRange | undefined = timeRange ? { ...timeRange } : undefined; - (searchSource as any).rawResponse = response; - } catch (e) { - // Log any error during request to the inspector - if (request) { - request.error({ json: e }); + currentAggs.setTimeRange(currentTimeRange as TimeRange); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return (searchSource.history = history); + }, + }); + + requestSearchSource.setField('aggs', function () { + return currentAggs.toDsl(metricsAtAllLevels); + }); + + requestSearchSource.onRequestStart((paramSearchSource, options) => { + return currentAggs.onSearchRequestStart(paramSearchSource, options); + }); + + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (currentTimeRange && allTimeFields.length > 0) { + timeFilterSearchSource.setField('filter', () => { + return allTimeFields + .map((fieldName) => getTime(indexPattern, currentTimeRange, { fieldName, forceNow })) + .filter(isRangeFilter); + }); } - throw e; - } finally { - // Add the request body no matter if things went fine or not - if (request) { - request.json(await requestSearchSource.getSearchRequestBody()); + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + let request; + if (inspectorAdapters.requests) { + inspectorAdapters.requests.reset(); + request = inspectorAdapters.requests.start( + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + searchSessionId, + } + ); + request.stats(getRequestInspectorStats(requestSearchSource)); } - } - // Note that rawResponse is not deeply cloned here, so downstream applications using courier - // must take care not to mutate it, or it could have unintended side effects, e.g. displaying - // response data incorrectly in the inspector. - let response = (searchSource as any).rawResponse; - for (const agg of aggs.aggs) { - if (agg.enabled && typeof agg.type.postFlightRequest === 'function') { - response = await agg.type.postFlightRequest( - response, - aggs, - agg, - requestSearchSource, - inspectorAdapters.requests, + try { + const response = await requestSearchSource.fetch({ abortSignal, - searchSessionId - ); + sessionId: searchSessionId, + }); + + if (request) { + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); + } + (requestSearchSource as any).rawResponse = response; + } catch (e) { + // Log any error during request to the inspector + if (request) { + request.error({ json: e }); + } + throw e; + } finally { + // Add the request body no matter if things went fine or not + if (request) { + request.json(await requestSearchSource.getSearchRequestBody()); + } } - } - const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; - const tabifyParams = { - metricsAtAllLevels, - partialRows, - timeRange: parsedTimeRange - ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } - : undefined, + // Note that rawResponse is not deeply cloned here, so downstream applications using courier + // must take care not to mutate it, or it could have unintended side effects, e.g. displaying + // response data incorrectly in the inspector. + let response = (requestSearchSource as any).rawResponse; + for (const agg of currentAggs.aggs) { + if (agg.enabled && typeof agg.type.postFlightRequest === 'function') { + response = await agg.type.postFlightRequest( + response, + currentAggs, + agg, + requestSearchSource, + inspectorAdapters.requests, + abortSignal, + searchSessionId + ); + } + } + + const parsedTimeRange = currentTimeRange + ? calculateBounds(currentTimeRange, { forceNow }) + : null; + const tabifyParams = { + metricsAtAllLevels, + partialRows, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, + }; + + const tabifiedResponse = tabifyAggResponse(currentAggs, response, tabifyParams); + return tabifiedResponse; }; - const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams); + const baseResponse = await buildBaseResponse(); - return tabifiedResponse; + const filterableAggLength = aggs.aggs.filter((agg) => agg.schema === 'segment').length; + + const partialResponses = await Promise.all( + Object.values(timeShifts).map(async (timeShift) => { + const currentAggs = aggs.clone(); + // assuming those are ordered correctly + const filterableAggs = currentAggs.aggs.filter((agg) => agg.schema === 'segment'); + const rootFilterAgg = currentAggs.createAggConfig( + { + type: 'filters', + id: 'rootSerialization', + params: { + filters: baseResponse.rows.map((row, rowIndex) => { + const filter: Filter = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + bool: { + filter: filterableAggs.map((a, i) => { + return a.getTimeShiftedFilter(timeShift, row[baseResponse.columns[i].id]); + }), + }, + }, + }; + return { + input: { + query: filter, + language: 'rawQuery', + }, + label: `serialized-row-${rowIndex}`, + id: `serialized-row-${rowIndex}`, + }; + }), + }, + enabled: true, + }, + { + addToAggConfigs: true, + } + ); + currentAggs.aggs = [ + rootFilterAgg, + ...currentAggs.aggs.filter((agg) => agg.schema === 'metric'), + ]; + // Create a new search source that inherits the original search source + // but has the appropriate timeRange applied via a filter. + // This is a temporary solution until we properly pass down all required + // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). + // Using callParentStartHandlers: true we make sure, that the parent searchSource + // onSearchRequestStart will be called properly even though we use an inherited + // search source. + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ + callParentStartHandlers: true, + }); + + const currentTimeRange: TimeRange | undefined = timeRange ? { ...timeRange } : undefined; + + if (currentTimeRange) { + if (currentTimeRange.from) { + currentTimeRange.from = moment(currentTimeRange.from).subtract(timeShift).toISOString(); + } + + if (currentTimeRange.from) { + currentTimeRange.to = moment(currentTimeRange.to).subtract(timeShift).toISOString(); + } + } + + currentAggs.setTimeRange(currentTimeRange as TimeRange); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return (searchSource.history = history); + }, + }); + + requestSearchSource.setField('aggs', function () { + return currentAggs.toDsl(metricsAtAllLevels); + }); + + requestSearchSource.onRequestStart((paramSearchSource, options) => { + return currentAggs.onSearchRequestStart(paramSearchSource, options); + }); + + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (currentTimeRange && allTimeFields.length > 0) { + timeFilterSearchSource.setField('filter', () => { + return allTimeFields + .map((fieldName) => getTime(indexPattern, currentTimeRange, { fieldName, forceNow })) + .filter(isRangeFilter); + }); + } + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + let request; + if (inspectorAdapters.requests) { + inspectorAdapters.requests.reset(); + request = inspectorAdapters.requests.start( + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + searchSessionId, + } + ); + request.stats(getRequestInspectorStats(requestSearchSource)); + } + + try { + const response = await requestSearchSource.fetch({ + abortSignal, + sessionId: searchSessionId, + }); + + if (request) { + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); + } + + if (!timeShift) { + (searchSource as any).rawResponse = response; + } + (requestSearchSource as any).rawResponse = response; + } catch (e) { + // Log any error during request to the inspector + if (request) { + request.error({ json: e }); + } + throw e; + } finally { + // Add the request body no matter if things went fine or not + if (request) { + request.json(await requestSearchSource.getSearchRequestBody()); + } + } + + // Note that rawResponse is not deeply cloned here, so downstream applications using courier + // must take care not to mutate it, or it could have unintended side effects, e.g. displaying + // response data incorrectly in the inspector. + let response = (requestSearchSource as any).rawResponse; + for (const agg of currentAggs.aggs) { + if (agg.enabled && typeof agg.type.postFlightRequest === 'function') { + response = await agg.type.postFlightRequest( + response, + currentAggs, + agg, + requestSearchSource, + inspectorAdapters.requests, + abortSignal, + searchSessionId + ); + } + } + + const parsedTimeRange = currentTimeRange + ? calculateBounds(currentTimeRange, { forceNow }) + : null; + const tabifyParams = { + metricsAtAllLevels, + partialRows, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, + }; + + const tabifiedResponse = tabifyAggResponse(currentAggs, response, tabifyParams); + + return tabifiedResponse; + }) + ); + + // todo - do an outer join on all partial responses + if (partialResponses.length > 0) { + // fullResponse.rows.forEach((row) => { + // fullResponse.columns.forEach((column) => { + // const columnAgg = aggs.aggs.find((a) => a.id === column.meta.sourceParams.id)!; + // if ( + // columnAgg.getTimeShift()?.asMilliseconds() !== fullResponseTimeShift?.asMilliseconds() + // ) { + // delete row[column.id]; + // } + // }); + // }); + // const joinAggs = aggs + // .bySchemaName('bucket') + // .filter((agg) => !timeFields || !timeFields.includes(agg.fieldName())); + // const joinColumns = fullResponse.columns.filter( + // (c) => + // c.meta.sourceParams.schema !== 'metric' && + // (!timeFields || !timeFields.includes(c.meta.sourceParams?.params?.field)) + // ); + // const timeJoinAggs = aggs + // .bySchemaName('bucket') + // .filter((agg) => timeFields && timeFields.includes(agg.fieldName())); + // const timeJoinColumns = fullResponse.columns.filter( + // (c) => + // c.meta.sourceParams.schema !== 'metric' && + // timeFields && + // timeFields.includes(c.meta.sourceParams?.params?.field) + // ); + partialResponses.forEach((partialResponse, index) => { + const timeShift = Object.values(timeShifts)[index]; + const missingColIndexes: number[] = []; + partialResponse.columns.forEach((column, colIndex) => { + // skip the serialized filters agg + if (colIndex === 0) { + return; + } + const columnAgg = aggs.aggs.find((a) => a.id === column.meta.sourceParams.id)!; + if (columnAgg.getTimeShift()?.asMilliseconds() === timeShift?.asMilliseconds()) { + missingColIndexes.push(colIndex - 1 + filterableAggLength); + } + }); + partialResponse.rows.forEach((row, i2) => { + const targetRowIndex = i2; + missingColIndexes.forEach((baseResponseIndex) => { + baseResponse.rows[targetRowIndex][baseResponse.columns[baseResponseIndex].id] = + row[partialResponse.columns[baseResponseIndex + 1 - filterableAggLength].id]; + }); + }); + }); + return baseResponse; + } else { + return baseResponse; + } }; +function getColumnIdentifier( + joinColumns: DatatableColumn[], + row: DatatableRow, + timeJoinColumns: DatatableColumn[], + timeShift: moment.Duration | undefined +) { + const joinStr = joinColumns.map((c) => String(row[c.id])); + const timeJoinStr = timeJoinColumns.map((c) => + String(moment(row[c.id]).add(timeShift).valueOf()) + ); + return joinStr.join(',') + timeJoinStr.join(','); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a54901a2a2fe1d..1595e88bf98ecb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -45,7 +45,6 @@ export function FrameLayout(props: FrameLayoutProps) { {props.workspacePanel} - {props.suggestionsPanel}
{