Skip to content

Commit

Permalink
[ES|QL] Filter by brushing a date histogram (#184012)
Browse files Browse the repository at this point in the history
## Summary

Part of #183425

Adds support of brushing an ES|QL chart. It updates the timepicker.

![meow
(2)](https://github.com/elastic/kibana/assets/17003240/05ac4e92-ea88-4ab7-93f7-3e7c9e300176)


### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Marco Liberati <[email protected]>
  • Loading branch information
stratoula and dej611 authored May 27, 2024
1 parent abd55fb commit a2c2f65
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PaletteRegistry } from '@kbn/coloring';
import { RenderMode } from '@kbn/expressions-plugin/common';
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { EmptyPlaceholder, LegendToggle } from '@kbn/charts-plugin/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
Expand Down Expand Up @@ -403,6 +404,7 @@ export function XYChart({
const defaultXScaleType = isTimeViz ? XScaleTypes.TIME : XScaleTypes.ORDINAL;

const isHistogramViz = dataLayers.every((l) => l.isHistogram);
const isEsqlMode = dataLayers.some((l) => l.table?.meta?.type === ESQL_TABLE_TYPE);
const hasBars = dataLayers.some((l) => l.seriesType === SeriesTypes.BAR);

const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain(
Expand Down Expand Up @@ -651,7 +653,12 @@ export function XYChart({
: undefined;
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === xAccessor);

const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex };
const context: BrushEvent['data'] = {
range: [min, max],
table,
column: xAxisColumnIndex,
...(isEsqlMode ? { timeFieldName: table.columns[xAxisColumnIndex].name } : {}),
};
onSelectRange(context);
};

Expand Down Expand Up @@ -779,7 +786,7 @@ export function XYChart({
formattedDatatables,
xAxisFormatter,
formatFactory,
interactive && !args.detailedTooltip
interactive && !args.detailedTooltip && !isEsqlMode
)}
customTooltip={
args.detailedTooltip
Expand Down Expand Up @@ -855,8 +862,9 @@ export function XYChart({
allowBrushingLastHistogramBin={isTimeViz}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
// enable brushing only for time charts, for both ES|QL and DSL queries
onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined}
onElementClick={interactive ? clickHandler : undefined}
onElementClick={interactive && !isEsqlMode ? clickHandler : undefined}
legendAction={
interactive
? getLegendAction(
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/data/common/search/expressions/esql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { ESQLSearchReponse, ESQLSearchParams } from '@kbn/es-types';
import { ESQL_LATEST_VERSION } from '@kbn/esql-utils';
import { getEsQueryConfig } from '../../es_query';
import { getTime } from '../../query';
import { ESQL_ASYNC_SEARCH_STRATEGY, KibanaContext } from '..';
import { ESQL_ASYNC_SEARCH_STRATEGY, KibanaContext, ESQL_TABLE_TYPE } from '..';
import { UiSettingsCommon } from '../..';

type Input = KibanaContext | null;
Expand Down Expand Up @@ -270,7 +270,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
return {
type: 'datatable',
meta: {
type: 'es_ql',
type: ESQL_TABLE_TYPE,
},
columns: allColumns,
rows,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export const ESQL_SEARCH_STRATEGY = 'esql';
export const ESQL_ASYNC_SEARCH_STRATEGY = 'esql_async';
export const ESQL_TABLE_TYPE = 'es_ql';
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

import moment from 'moment';

import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select';
import {
createFiltersFromRangeSelectAction,
type RangeSelectDataContext,
} from './create_filters_from_range_select';

import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { dataPluginMock } from '../../mocks';
Expand All @@ -20,12 +23,8 @@ import { RangeFilter } from '@kbn/es-query';
describe('brushEvent', () => {
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const JAN_01_2014 = 1388559600000;
let baseEvent: {
table: any;
column: number;
range: number[];
timeFieldName?: string;
};
let baseEvent: RangeSelectDataContext;
let esqlEventContext: RangeSelectDataContext;

const mockField = {
name: 'time',
Expand Down Expand Up @@ -82,6 +81,28 @@ describe('brushEvent', () => {
},
range: [],
};

esqlEventContext = {
column: 0,
query: { esql: 'FROM indexPatternId | limit 10' },
table: {
type: 'datatable',
meta: {
type: 'es_ql',
},
columns: [
{
id: '1',
name: '1',
meta: {
type: 'date',
},
},
],
rows: [],
},
range: [],
};
});

test('should be a function', () => {
Expand Down Expand Up @@ -197,4 +218,33 @@ describe('brushEvent', () => {
}
});
});

describe('handles an event for an ES_QL query', () => {
afterAll(() => {
esqlEventContext.range = [];
});

test('by ignoring the event when range does not span at least 2 values', async () => {
esqlEventContext.range = [JAN_01_2014];
const filter = await createFiltersFromRangeSelectAction(esqlEventContext);
expect(filter).toEqual([]);
});

test('by creating a new filter', async () => {
const rangeBegin = JAN_01_2014;
const rangeEnd = rangeBegin + DAY_IN_MS;
esqlEventContext.range = [rangeBegin, rangeEnd];
const filter = await createFiltersFromRangeSelectAction(esqlEventContext);

expect(filter).toBeDefined();

if (filter.length) {
const rangeFilter = filter[0] as RangeFilter;
expect(rangeFilter.meta.index).toBeUndefined();
expect(rangeFilter.query.range['1'].gte).toBe(moment(rangeBegin).toISOString());
expect(rangeFilter.query.range['1'].lt).toBe(moment(rangeEnd).toISOString());
expect(rangeFilter.query.range['1']).toHaveProperty('format', 'strict_date_optional_time');
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,65 @@
import { last } from 'lodash';
import moment from 'moment';
import { Datatable } from '@kbn/expressions-plugin/common';
import { type AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { buildRangeFilter, DataViewFieldBase, RangeFilterParams } from '@kbn/es-query';
import { getIndexPatterns, getSearchService } from '../../services';
import { AggConfigSerialized } from '../../../common/search/aggs';
import { mapAndFlattenFilters } from '../../query';

interface RangeSelectDataContext {
export interface RangeSelectDataContext {
table: Datatable;
column: number;
range: number[];
timeFieldName?: string;
query?: AggregateQuery;
}

const getParameters = async (event: RangeSelectDataContext) => {
const column: Record<string, any> = event.table.columns[event.column];
// Handling of the ES|QL datatable
if (isOfAggregateQueryType(event.query)) {
const field = new DataViewField({
name: column.name,
type: column.meta?.type ?? 'unknown',
esTypes: column.meta?.esType ? ([column.meta.esType] as string[]) : undefined,
searchable: true,
aggregatable: false,
});

return {
field,
indexPattern: undefined,
};
}
if (column.meta && 'sourceParams' in column.meta) {
const { indexPatternId, ...aggConfigs } = column.meta.sourceParams;
const indexPattern = await getIndexPatterns().get(indexPatternId);
const aggConfigsInstance = getSearchService().aggs.createAggConfigs(indexPattern, [
aggConfigs as AggConfigSerialized,
]);
const aggConfig = aggConfigsInstance.aggs[0];
const field: DataViewFieldBase = aggConfig.params.field;
return {
field,
indexPattern,
};
}
return {
field: undefined,
indexPattern: undefined,
};
};

export async function createFiltersFromRangeSelectAction(event: RangeSelectDataContext) {
const column: Record<string, any> = event.table.columns[event.column];

if (!column || !column.meta) {
return [];
}

const { indexPatternId, ...aggConfigs } = column.meta.sourceParams;
const indexPattern = await getIndexPatterns().get(indexPatternId);
const aggConfigsInstance = getSearchService().aggs.createAggConfigs(indexPattern, [
aggConfigs as AggConfigSerialized,
]);
const aggConfig = aggConfigsInstance.aggs[0];
const field: DataViewFieldBase = aggConfig.params.field;
const { field, indexPattern } = await getParameters(event);

if (!field || event.range.length <= 1) {
return [];
Expand All @@ -57,6 +90,5 @@ export async function createFiltersFromRangeSelectAction(event: RangeSelectDataC
if (isDate) {
range.format = 'strict_date_optional_time';
}

return mapAndFlattenFilters([buildRangeFilter(field, range, indexPattern)]);
}
2 changes: 2 additions & 0 deletions src/plugins/data/public/actions/select_range_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import type { AggregateQuery } from '@kbn/es-query';
import { Datatable } from '@kbn/expressions-plugin/public';
import { UiActionsActionDefinition, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { APPLY_FILTER_TRIGGER } from '../triggers';
Expand All @@ -20,6 +21,7 @@ export interface SelectRangeActionContext {
column: number;
range: number[];
timeFieldName?: string;
query?: AggregateQuery;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, { useCallback, useMemo } from 'react';
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
import type { Datatable } from '@kbn/expressions-plugin/common';
import { useDiscoverHistogram } from './use_discover_histogram';
import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content';
Expand Down Expand Up @@ -61,6 +62,9 @@ export const DiscoverHistogramLayout = ({
type: 'datatable' as 'datatable',
rows: datatable.result!.map((r) => r.raw),
columns: datatable.esqlQueryColumns || [],
meta: {
type: ESQL_TABLE_TYPE,
},
};
}
}, [datatable, isEsqlMode]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,47 @@ describe('Textbased Data Source', () => {
isBucketed: false,
hasTimeShift: false,
hasReducedTimeRange: false,
scale: 'ratio',
});
});

it('should get an operation for col2', () => {
const state = {
layers: {
a: {
columns: [
{
columnId: 'col1',
fieldName: 'Test 1',
meta: {
type: 'number',
},
},
{
columnId: 'col2',
fieldName: 'Test 2',
meta: {
type: 'date',
},
},
],
index: 'foo',
},
},
} as unknown as TextBasedPrivateState;

publicAPI = TextBasedDatasource.getPublicAPI({
state,
layerId: 'a',
indexPatterns,
});
expect(publicAPI.getOperationForColumnId('col2')).toEqual({
label: 'Test 2',
dataType: 'date',
isBucketed: true,
hasTimeShift: false,
hasReducedTimeRange: false,
scale: 'interval',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DatasourceDimensionTriggerProps,
DataSourceInfo,
UserMessage,
OperationMetadata,
} from '../../types';
import { generateId } from '../../id_generator';
import type {
Expand Down Expand Up @@ -533,6 +534,18 @@ export function getTextBasedDatasource({
const layer = state.layers[layerId];
const column = layer?.columns?.find((c) => c.columnId === columnId);
const columnLabelMap = TextBasedDatasource.uniqueLabels(state, indexPatterns);
let scale: OperationMetadata['scale'] = 'ordinal';
switch (column?.meta?.type) {
case 'date':
scale = 'interval';
break;
case 'number':
scale = 'ratio';
break;
default:
scale = 'ordinal';
break;
}

if (column) {
return {
Expand All @@ -542,6 +555,7 @@ export function getTextBasedDatasource({
inMetricDimension: column.inMetricDimension,
hasTimeShift: false,
hasReducedTimeRange: false,
scale,
};
}
return null;
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/lens/public/embeddable/embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ export class Embeddable
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
onRender$={this.onRender}
interactive={!input.disableTriggers && !this.isTextBasedLanguage()}
interactive={!input.disableTriggers}
renderMode={input.renderMode}
syncColors={input.syncColors}
syncTooltips={input.syncTooltips}
Expand Down Expand Up @@ -1369,6 +1369,7 @@ export class Embeddable
} else if (isLensTableRowContextMenuClickEvent(event)) {
eventHandler = this.input.onTableRowClick;
}
const esqlQuery = this.isTextBasedLanguage() ? this.savedVis?.state.query : undefined;

eventHandler?.({
...event.data,
Expand All @@ -1384,6 +1385,7 @@ export class Embeddable
...event.data,
timeFieldName:
event.data.timeFieldName || inferTimeField(this.deps.data.datatableUtilities, event),
query: esqlQuery,
},
embeddable: this,
});
Expand Down

0 comments on commit a2c2f65

Please sign in to comment.