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:
parent
37ec1e1053
commit
8e2cb5684e
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue