KQL support in filter ratio in TSVB (#75033)

* KQL support in filter ratio in TSVB

Closes #67503

* Fix filter_ratio and filter_ratios tests

* fix JEST

* Refactor some code in filter_ratio, filter_ratios, filter_ratios.test

* Edit query value in filter_ratio and filter_ratios.test

* Refacor some code in filter_ratio.js and visualization_migrations.ts

* Remove duplications in vis_schema and refactor filter_ratio

* Refactor filter_ratio.js

* Update default query with getDefaultQuery()

* Fix filter_ratio and histogram_support tests

Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Diana Derevyankina 2020-08-27 13:35:04 +03:00 committed by GitHub
parent 37ec1e1053
commit 8e2cb5684e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 194 additions and 42 deletions

View file

@ -80,8 +80,8 @@ export const metricsItems = schema.object({
field: stringOptionalNullable,
id: stringRequired,
metric_agg: stringOptionalNullable,
numerator: stringOptionalNullable,
denominator: stringOptionalNullable,
numerator: schema.maybe(queryObject),
denominator: schema.maybe(queryObject),
sigma: stringOptionalNullable,
unit: stringOptionalNullable,
model_type: stringOptionalNullable,
@ -128,12 +128,7 @@ export const metricsItems = schema.object({
const splitFiltersItems = schema.object({
id: stringOptionalNullable,
color: stringOptionalNullable,
filter: schema.maybe(
schema.object({
language: schema.string(),
query: schema.string(),
})
),
filter: schema.maybe(queryObject),
label: stringOptionalNullable,
});

View file

@ -18,25 +18,25 @@
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { AggSelect } from './agg_select';
import { FieldSelect } from './field_select';
import { AggRow } from './agg_row';
import { createChangeHandler } from '../lib/create_change_handler';
import { createSelectHandler } from '../lib/create_select_handler';
import { createTextHandler } from '../lib/create_text_handler';
import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiFieldText,
EuiSpacer,
EuiFormRow,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public';
import { getSupportedFieldsByMetricType } from '../lib/get_supported_fields_by_metric_type';
import { getDataStart } from '../../../services';
import { QueryBarWrapper } from '../query_bar_wrapper';
const isFieldHistogram = (fields, indexPattern, field) => {
const indexFields = fields[indexPattern];
@ -49,15 +49,24 @@ const isFieldHistogram = (fields, indexPattern, field) => {
export const FilterRatioAgg = (props) => {
const { series, fields, panel } = props;
const handleChange = createChangeHandler(props.onChange, props.model);
const handleChange = useMemo(() => createChangeHandler(props.onChange, props.model), [
props.model,
props.onChange,
]);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const handleNumeratorQueryChange = useCallback((query) => handleChange({ numerator: query }), [
handleChange,
]);
const handleDenominatorQueryChange = useCallback(
(query) => handleChange({ denominator: query }),
[handleChange]
);
const indexPattern =
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;
const defaults = {
numerator: '*',
denominator: '*',
numerator: getDataStart().query.queryString.getDefaultQuery(),
denominator: getDataStart().query.queryString.getDefaultQuery(),
metric_agg: 'count',
};
@ -101,7 +110,11 @@ export const FilterRatioAgg = (props) => {
/>
}
>
<EuiFieldText onChange={handleTextChange('numerator')} value={model.numerator} />
<QueryBarWrapper
query={model.numerator}
onChange={handleNumeratorQueryChange}
indexPatterns={[indexPattern]}
/>
</EuiFormRow>
</EuiFlexItem>
@ -115,7 +128,11 @@ export const FilterRatioAgg = (props) => {
/>
}
>
<EuiFieldText onChange={handleTextChange('denominator')} value={model.denominator} />
<QueryBarWrapper
query={model.denominator}
onChange={handleDenominatorQueryChange}
indexPatterns={[indexPattern]}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -22,8 +22,16 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { FilterRatioAgg } from './filter_ratio';
import { FIELDS, METRIC, SERIES, PANEL } from '../../../test_utils';
import { EuiComboBox } from '@elastic/eui';
import { dataPluginMock } from '../../../../../data/public/mocks';
import { setDataStart } from '../../../services';
jest.mock('../query_bar_wrapper', () => ({
QueryBarWrapper: jest.fn(() => null),
}));
describe('TSVB Filter Ratio', () => {
beforeAll(() => setDataStart(dataPluginMock.createStartContract()));
const setup = (metric) => {
const series = { ...SERIES, metrics: [metric] };
const panel = { ...PANEL, series };

View file

@ -22,6 +22,13 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { Agg } from './agg';
import { FieldSelect } from './field_select';
import { FIELDS, METRIC, SERIES, PANEL } from '../../../test_utils';
import { setDataStart } from '../../../services';
import { dataPluginMock } from '../../../../../data/public/mocks';
jest.mock('../query_bar_wrapper', () => ({
QueryBarWrapper: jest.fn(() => null),
}));
const runTest = (aggType, name, test, additionalProps = {}) => {
describe(aggType, () => {
const metric = {
@ -55,6 +62,8 @@ const runTest = (aggType, name, test, additionalProps = {}) => {
};
describe('Histogram Types', () => {
beforeAll(() => setDataStart(dataPluginMock.createStartContract()));
describe('supported', () => {
const shouldHaveHistogramSupport = (aggType, additionalProps = {}) => {
runTest(

View file

@ -20,17 +20,22 @@
const filter = (metric) => metric.type === 'filter_ratio';
import { bucketTransform } from '../../helpers/bucket_transform';
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
export function ratios(req, panel, series) {
export function ratios(req, panel, series, esQueryConfig, indexPatternObject) {
return (next) => (doc) => {
if (series.metrics.some(filter)) {
series.metrics.filter(filter).forEach((metric) => {
overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`, {
query_string: { query: metric.numerator || '*', analyze_wildcard: true },
});
overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`, {
query_string: { query: metric.denominator || '*', analyze_wildcard: true },
});
overwrite(
doc,
`aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`,
esQuery.buildEsQuery(indexPatternObject, metric.numerator, [], esQueryConfig)
);
overwrite(
doc,
`aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`,
esQuery.buildEsQuery(indexPatternObject, metric.denominator, [], esQueryConfig)
);
let numeratorPath = `${metric.id}-numerator>_count`;
let denominatorPath = `${metric.id}-denominator>_count`;

View file

@ -19,10 +19,12 @@
import { ratios } from './filter_ratios';
describe('ratios(req, panel, series)', () => {
describe('ratios(req, panel, series, esQueryConfig, indexPatternObject)', () => {
let panel;
let series;
let req;
let esQueryConfig;
let indexPatternObject;
beforeEach(() => {
panel = {
time_field: 'timestamp',
@ -36,8 +38,8 @@ describe('ratios(req, panel, series)', () => {
{
id: 'metric-1',
type: 'filter_ratio',
numerator: 'errors',
denominator: '*',
numerator: { query: 'errors', language: 'lucene' },
denominator: { query: 'warnings', language: 'lucene' },
metric_agg: 'avg',
field: 'cpu',
},
@ -51,17 +53,23 @@ describe('ratios(req, panel, series)', () => {
},
},
};
esQueryConfig = {
allowLeadingWildcards: true,
queryStringOptions: { analyze_wildcard: true },
ignoreFilterIfFieldNotInIndex: false,
};
indexPatternObject = {};
});
test('calls next when finished', () => {
const next = jest.fn();
ratios(req, panel, series)(next)({});
ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns filter ratio aggs', () => {
const next = (doc) => doc;
const doc = ratios(req, panel, series)(next)({});
const doc = ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@ -88,9 +96,18 @@ describe('ratios(req, panel, series)', () => {
},
},
filter: {
query_string: {
analyze_wildcard: true,
query: '*',
bool: {
must: [
{
query_string: {
query: 'warnings',
analyze_wildcard: true,
},
},
],
filter: [],
should: [],
must_not: [],
},
},
},
@ -103,9 +120,18 @@ describe('ratios(req, panel, series)', () => {
},
},
filter: {
query_string: {
analyze_wildcard: true,
query: 'errors',
bool: {
must: [
{
query_string: {
query: 'errors',
analyze_wildcard: true,
},
},
],
filter: [],
should: [],
must_not: [],
},
},
},
@ -120,7 +146,7 @@ describe('ratios(req, panel, series)', () => {
test('returns empty object when field is not set', () => {
delete series.metrics[0].field;
const next = (doc) => doc;
const doc = ratios(req, panel, series)(next)({});
const doc = ratios(req, panel, series, esQueryConfig, indexPatternObject)(next)({});
expect(doc).toEqual({
aggs: {
test: {
@ -141,18 +167,36 @@ describe('ratios(req, panel, series)', () => {
'metric-1-denominator': {
aggs: { metric: {} },
filter: {
query_string: {
analyze_wildcard: true,
query: '*',
bool: {
must: [
{
query_string: {
query: 'warnings',
analyze_wildcard: true,
},
},
],
filter: [],
should: [],
must_not: [],
},
},
},
'metric-1-numerator': {
aggs: { metric: {} },
filter: {
query_string: {
analyze_wildcard: true,
query: 'errors',
bool: {
must: [
{
query_string: {
analyze_wildcard: true,
query: 'errors',
},
},
],
filter: [],
should: [],
must_not: [],
},
},
},

View file

@ -1544,4 +1544,38 @@ describe('migration visualization', () => {
expect(series[0].split_color_mode).toBeUndefined();
});
});
describe('7.10.0 tsvb filter_ratio migration', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.10.0'](
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const testDoc1 = {
attributes: {
title: 'My Vis',
description: 'This is my super cool vis.',
visState: `{"type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries",
"series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417",
"type":"filter_ratio","numerator":"Filter Bytes Test:>1000","denominator":"Filter Bytes Test:<1000"}]}]}}`,
},
};
it('should replace numerator string with a query object', () => {
const migratedTestDoc1 = migrate(testDoc1);
const metric = JSON.parse(migratedTestDoc1.attributes.visState).params.series[0].metrics[0];
expect(metric.numerator).toHaveProperty('query');
expect(metric.numerator).toHaveProperty('language');
});
it('should replace denominator string with a query object', () => {
const migratedTestDoc1 = migrate(testDoc1);
const metric = JSON.parse(migratedTestDoc1.attributes.visState).params.series[0].metrics[0];
expect(metric.denominator).toHaveProperty('query');
expect(metric.denominator).toHaveProperty('language');
});
});
});

View file

@ -99,6 +99,45 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn<any, any> = (doc)
return doc;
};
// [TSVB] Replace string query with object
const migrateFilterRatioQuery: SavedObjectMigrationFn<any, any> = (doc) => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState && visState.type === 'metrics') {
const series: any[] = get(visState, 'params.series') || [];
series.forEach((part) => {
(part.metrics || []).forEach((metric: any) => {
if (metric.type === 'filter_ratio') {
if (typeof metric.numerator === 'string') {
metric.numerator = { query: metric.numerator, language: 'lucene' };
}
if (typeof metric.denominator === 'string') {
metric.denominator = { query: metric.denominator, language: 'lucene' };
}
}
});
});
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(visState),
},
};
}
}
return doc;
};
// [TSVB] Remove stale opperator key
const migrateOperatorKeyTypo: SavedObjectMigrationFn<any, any> = (doc) => {
const visStateJSON = get(doc, 'attributes.visState');
@ -713,4 +752,5 @@ export const visualizationSavedObjectTypeMigrations = {
'7.4.2': flow(transformSplitFiltersStringToQueryObject),
'7.7.0': flow(migrateOperatorKeyTypo, migrateSplitByChartRow),
'7.8.0': flow(migrateTsvbDefaultColorPalettes),
'7.10.0': flow(migrateFilterRatioQuery),
};