Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates ES|QL where filters for ordinal charts #184420

Merged
merged 22 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React, { memo, FC, useMemo, useState, useCallback, useRef } from 'react';
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
import {
Chart,
ElementClickListener,
Expand Down Expand Up @@ -253,7 +254,9 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
datatables: [formattedTable.table],
});

const hasTooltipActions = interactive;
const isEsqlMode = table?.meta?.type === ESQL_TABLE_TYPE;

const hasTooltipActions = interactive && !isEsqlMode;

const onElementClick = useCallback(
(e: HeatmapElementEvent[]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Tooltip,
TooltipValue,
} from '@elastic/charts';
import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
import { i18n } from '@kbn/i18n';
import { useEuiTheme } from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
Expand Down Expand Up @@ -409,8 +410,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
? getColumnByAccessor(splitRow[0], visData.columns)
: undefined;

const isEsqlMode = originalVisData?.meta?.type === ESQL_TABLE_TYPE;
const hasTooltipActions =
interactive && bucketAccessors.filter((a) => a !== 'metric-name').length > 0;
interactive && !isEsqlMode && bucketAccessors.filter((a) => a !== 'metric-name').length > 0;

const tooltip: TooltipProps = {
...(fixedViewPort ? { boundary: fixedViewPort } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ export function XYChart({
xDomain={xDomain}
// enable brushing only for time charts, for both ES|QL and DSL queries
onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined}
onElementClick={interactive && !isEsqlMode ? clickHandler : undefined}
onElementClick={interactive ? clickHandler : undefined}
legendAction={
interactive
? getLegendAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,97 +9,167 @@
import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { dataPluginMock } from '../../mocks';
import { setIndexPatterns, setSearchService } from '../../services';
import { createFiltersFromValueClickAction } from './create_filters_from_value_click';
import {
createFiltersFromValueClickAction,
appendFilterToESQLQueryFromValueClickAction,
} from './create_filters_from_value_click';
import { FieldFormatsGetConfigFn, BytesFormat } from '@kbn/field-formats-plugin/common';
import { RangeFilter } from '@kbn/es-query';

const mockField = {
name: 'bytes',
filterable: true,
};
describe('createFiltersFromClickEvent', () => {
const dataStart = dataPluginMock.createStartContract();
setSearchService(dataStart.search);
setIndexPatterns({
...dataStart.indexPatterns,
get: async () => ({
id: 'logstash-*',
fields: {
getByName: () => mockField,
filter: () => [mockField],
},
getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn),
}),
} as unknown as DataViewsContract);
describe('createFiltersFromValueClick', () => {
let dataPoints: Parameters<typeof createFiltersFromValueClickAction>[0]['data'];

describe('createFiltersFromValueClick', () => {
let dataPoints: Parameters<typeof createFiltersFromValueClickAction>[0]['data'];

beforeEach(() => {
dataPoints = [
{
table: {
columns: [
{
name: 'test',
id: '1-1',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
indexPatternId: 'logstash-*',
type: 'histogram',
params: {
field: 'bytes',
interval: 30,
otherBucket: true,
beforeEach(() => {
dataPoints = [
{
table: {
columns: [
{
name: 'test',
id: '1-1',
meta: {
type: 'date',
source: 'esaggs',
sourceParams: {
indexPatternId: 'logstash-*',
type: 'histogram',
params: {
field: 'bytes',
interval: 30,
otherBucket: true,
},
},
},
},
},
],
rows: [
{
'1-1': '2048',
},
],
},
column: 0,
row: 0,
value: 'test',
},
];

const dataStart = dataPluginMock.createStartContract();
setSearchService(dataStart.search);
setIndexPatterns({
...dataStart.indexPatterns,
get: async () => ({
id: 'logstash-*',
fields: {
getByName: () => mockField,
filter: () => [mockField],
],
rows: [
{
'1-1': '2048',
},
],
},
column: 0,
row: 0,
value: 'test',
},
getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn),
}),
} as unknown as DataViewsContract);
});
];
});
test('ignores event when value for rows is not provided', async () => {
dataPoints[0].table.rows[0]['1-1'] = null;
const filters = await createFiltersFromValueClickAction({ data: dataPoints });

test('ignores event when value for rows is not provided', async () => {
dataPoints[0].table.rows[0]['1-1'] = null;
const filters = await createFiltersFromValueClickAction({ data: dataPoints });
expect(filters.length).toEqual(0);
});

expect(filters.length).toEqual(0);
});
test('handles an event when aggregations type is a terms', async () => {
(dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms';
const filters = await createFiltersFromValueClickAction({ data: dataPoints });

test('handles an event when aggregations type is a terms', async () => {
(dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms';
const filters = await createFiltersFromValueClickAction({ data: dataPoints });
expect(filters.length).toEqual(1);
expect(filters[0].query?.match_phrase?.bytes).toEqual('2048');
});

expect(filters.length).toEqual(1);
expect(filters[0].query?.match_phrase?.bytes).toEqual('2048');
});
test('handles an event when aggregations type is not terms', async () => {
const filters = await createFiltersFromValueClickAction({ data: dataPoints });

test('handles an event when aggregations type is not terms', async () => {
const filters = await createFiltersFromValueClickAction({ data: dataPoints });
expect(filters.length).toEqual(1);

expect(filters.length).toEqual(1);
const [rangeFilter] = filters as RangeFilter[];
expect(rangeFilter.query.range.bytes.gte).toEqual(2048);
expect(rangeFilter.query.range.bytes.lt).toEqual(2078);
});

const [rangeFilter] = filters as RangeFilter[];
expect(rangeFilter.query.range.bytes.gte).toEqual(2048);
expect(rangeFilter.query.range.bytes.lt).toEqual(2078);
test('handles non-unique filters', async () => {
const [point] = dataPoints;
const filters = await createFiltersFromValueClickAction({ data: [point, point] });

expect(filters.length).toEqual(1);
});
});
describe('appendFilterToESQLQueryFromValueClickAction', () => {
let dataPoints: Parameters<typeof appendFilterToESQLQueryFromValueClickAction>[0]['data'];
beforeEach(() => {
dataPoints = [
{
table: {
columns: [
{
name: 'columnA',
id: 'columnA',
meta: {
type: 'date',
},
},
],
rows: [
{
columnA: '2048',
},
],
},
column: 0,
row: 0,
value: 'test',
},
];
});
test('should return null for date fields', async () => {
const queryString = await appendFilterToESQLQueryFromValueClickAction({
data: dataPoints,
query: { esql: 'from meow' },
});

expect(queryString).toBeUndefined();
});

test('handles non-unique filters', async () => {
const [point] = dataPoints;
const filters = await createFiltersFromValueClickAction({ data: [point, point] });
test('should return null if no aggregate query is present', async () => {
dataPoints[0].table.columns[0] = {
name: 'test',
id: '1-1',
meta: {
type: 'string',
},
};
const queryString = await appendFilterToESQLQueryFromValueClickAction({
data: dataPoints,
});

expect(queryString).toBeUndefined();
});

test('should return the update query string', async () => {
dataPoints[0].table.columns[0] = {
name: 'columnA',
id: 'columnA',
meta: {
type: 'string',
},
};
const queryString = await appendFilterToESQLQueryFromValueClickAction({
data: dataPoints,
query: { esql: 'from meow' },
});

expect(filters.length).toEqual(1);
expect(queryString).toEqual(`from meow
| where \`columnA\`=="2048"`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

import _ from 'lodash';
import { Datatable } from '@kbn/expressions-plugin/public';
import { compareFilters, COMPARE_ALL_OPTIONS, Filter, toggleFilterNegated } from '@kbn/es-query';
import {
compareFilters,
COMPARE_ALL_OPTIONS,
Filter,
toggleFilterNegated,
type AggregateQuery,
} from '@kbn/es-query';
import { appendWhereClauseToESQLQuery } from '@kbn/esql-utils';
import { getIndexPatterns, getSearchService } from '../../services';
import { AggConfigSerialized } from '../../../common/search/aggs';
import { mapAndFlattenFilters } from '../../query';
Expand All @@ -22,6 +29,7 @@ interface ValueClickDataContext {
}>;
timeFieldName?: string;
negate?: boolean;
query?: AggregateQuery;
}

/**
Expand Down Expand Up @@ -148,3 +156,40 @@ export const createFiltersFromValueClickAction = async ({
compareFilters(a, b, COMPARE_ALL_OPTIONS)
);
};

/** @public */
export const appendFilterToESQLQueryFromValueClickAction = async ({
data,
query,
}: ValueClickDataContext) => {
let queryWithFilter: string | undefined;

await Promise.all(
stratoula marked this conversation as resolved.
Show resolved Hide resolved
data
.filter((point) => point)
.map(async (val) => {
const { table, column: columnIndex, row: rowIndex } = val;
if (table && table.columns && table.columns[columnIndex]) {
const column = table.columns[columnIndex];
const value: unknown = rowIndex > -1 ? table.rows[rowIndex][column.id] : null;

// Do not append in case of time series, for now. We need to find a way to compute the interval
// to create the time range filter correctly. The users can brush to update the time filter instead.
const isDate = column.meta?.type === 'date';

if (value == null || isDate || !query) return;

const queryString = query.esql;
queryWithFilter = appendWhereClauseToESQLQuery(
queryString,
column.id,
stratoula marked this conversation as resolved.
Show resolved Hide resolved
value,
'+',
column.meta?.type
);
}
})
);

return queryWithFilter;
};
Loading