[Filters] Add support for combined filter query DSL editing (#148590)

## Summary

Resolves https://github.com/elastic/kibana/issues/144601.

Enables "Edit as Query DSL" for combined filters (those created with
complex AND/OR relationships).

### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
This commit is contained in:
Lukas Olson 2023-01-16 02:21:24 -07:00 committed by GitHub
parent 04f27948ac
commit 46a37101e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 51 additions and 56 deletions

View file

@ -51,6 +51,7 @@ export type {
export {
buildEsQuery,
buildQueryFromFilters,
filterToQueryDsl,
decorateQuery,
luceneStringToDsl,
migrateFilter,

View file

@ -71,32 +71,18 @@ export const buildQueryFromFilters = (
ignoreFilterIfFieldNotInIndex: false,
}
): BoolQuery => {
const { ignoreFilterIfFieldNotInIndex = false, nestedIgnoreUnmapped } = options;
const { ignoreFilterIfFieldNotInIndex = false } = options;
const filters = inputFilters.filter((filter) => filter && !isFilterDisabled(filter));
const indexPatterns = Array.isArray(inputDataViews) ? inputDataViews : [inputDataViews];
const findIndexPattern = (id: string | undefined) => {
return indexPatterns.find((index) => index?.id === id) || indexPatterns[0];
};
const filtersToESQueries = (negate: boolean) => {
return filters
.filter((f) => !!f)
.filter(filterNegate(negate))
.filter((filter) => {
const indexPattern = findIndexPattern(filter.meta?.index);
const indexPattern = findIndexPattern(inputDataViews, filter.meta?.index);
return !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern);
})
.map((filter) => {
const indexPattern = findIndexPattern(filter.meta?.index);
const migratedFilter = migrateFilter(filter, indexPattern);
return fromNestedFilter(migratedFilter, indexPattern, {
ignoreUnmapped: nestedIgnoreUnmapped,
});
})
.map((filter) => fromCombinedFilter(filter, inputDataViews, options))
.map(cleanFilter)
.map(translateToQuery);
.map((filter) => filterToQueryDsl(filter, inputDataViews, options));
};
return {
@ -106,3 +92,24 @@ export const buildQueryFromFilters = (
must_not: filtersToESQueries(true),
};
};
function findIndexPattern(
inputDataViews: DataViewBase | DataViewBase[] | undefined,
id: string | undefined
) {
const dataViews = Array.isArray(inputDataViews) ? inputDataViews : [inputDataViews];
return dataViews.find((index) => index?.id === id) ?? dataViews[0];
}
export function filterToQueryDsl(
filter: Filter,
inputDataViews: DataViewBase | DataViewBase[] | undefined,
options: EsQueryFiltersConfig = {}
) {
const indexPattern = findIndexPattern(inputDataViews, filter.meta?.index);
const migratedFilter = migrateFilter(filter, indexPattern);
const nestedFilter = fromNestedFilter(migratedFilter, indexPattern, options);
const combinedFilter = fromCombinedFilter(nestedFilter, inputDataViews, options);
const cleanedFilter = cleanFilter(combinedFilter);
return translateToQuery(cleanedFilter);
}

View file

@ -42,7 +42,7 @@ describe('fromNestedFilter', function () {
it('should allow to configure ignore_unmapped', () => {
const field = getField('nestedField.child');
const filter = buildPhraseFilter(field!, 'foo', indexPattern);
const result = fromNestedFilter(filter, indexPattern, { ignoreUnmapped: true });
const result = fromNestedFilter(filter, indexPattern, { nestedIgnoreUnmapped: true });
expect(result).toEqual({
meta: {
index: 'logstash-*',

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { EsQueryFiltersConfig } from '../..';
import { getFilterField, cleanFilter, Filter } from '../filters';
import { DataViewBase } from './types';
import { getDataViewFieldSubtypeNested } from '../utils';
@ -14,7 +15,7 @@ import { getDataViewFieldSubtypeNested } from '../utils';
export const fromNestedFilter = (
filter: Filter,
indexPattern?: DataViewBase,
config: { ignoreUnmapped?: boolean } = {}
config: EsQueryFiltersConfig = {}
) => {
if (!indexPattern) return filter;
@ -40,8 +41,8 @@ export const fromNestedFilter = (
nested: {
path: subTypeNested.nested.path,
query: query.query || query,
...(typeof config.ignoreUnmapped === 'boolean' && {
ignore_unmapped: config.ignoreUnmapped,
...(typeof config.nestedIgnoreUnmapped === 'boolean' && {
ignore_unmapped: config.nestedIgnoreUnmapped,
}),
},
},

View file

@ -10,7 +10,7 @@ export { migrateFilter } from './migrate_filter';
export type { EsQueryFiltersConfig } from './from_filters';
export type { EsQueryConfig } from './build_es_query';
export { buildEsQuery } from './build_es_query';
export { buildQueryFromFilters } from './from_filters';
export { buildQueryFromFilters, filterToQueryDsl } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
export {

View file

@ -25,14 +25,13 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
type Filter,
BooleanRelation,
buildCombinedFilter,
buildCustomFilter,
buildEmptyFilter,
cleanFilter,
Filter,
filterToQueryDsl,
getFilterParams,
isCombinedFilter,
} from '@kbn/es-query';
import { merge } from 'lodash';
import React, { Component } from 'react';
@ -76,10 +75,6 @@ export const strings = {
i18n.translate('unifiedSearch.filter.filterEditor.updateButtonLabel', {
defaultMessage: 'Update filter',
}),
getDisableToggleModeTooltip: () =>
i18n.translate('unifiedSearch.filter.filterEditor.disableToggleModeTooltip', {
defaultMessage: '"Edit as Query DSL" operation is not supported for combined filters',
}),
getSelectDataViewToolTip: () =>
i18n.translate('unifiedSearch.filter.filterEditor.chooseDataViewFirstToolTip', {
defaultMessage: 'You need to select a data view first',
@ -159,13 +154,11 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
}
private parseFilterToQueryDsl(filter: Filter) {
return JSON.stringify(cleanFilter(filter), null, 2);
const dsl = filterToQueryDsl(filter, this.props.indexPatterns);
return JSON.stringify(dsl, null, 2);
}
public render() {
const { localFilter } = this.state;
const shouldDisableToggle = isCombinedFilter(localFilter);
return (
<div>
<EuiPopoverTitle paddingSize="s">
@ -180,30 +173,23 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
</EuiFlexGroup>
<EuiFlexItem grow={false} className="filterEditor__hiddenItem" />
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={shouldDisableToggle ? strings.getDisableToggleModeTooltip() : null}
display="block"
<EuiButtonEmpty
size="xs"
data-test-subj="editQueryDSL"
onClick={this.toggleCustomEditor}
>
<EuiButtonEmpty
size="xs"
data-test-subj="editQueryDSL"
disabled={shouldDisableToggle}
onClick={this.toggleCustomEditor}
>
{this.state.isCustomEditorOpen ? (
<FormattedMessage
id="unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel"
defaultMessage="Edit filter values"
/>
) : (
<FormattedMessage
id="unifiedSearch.filter.filterEditor.editQueryDslButtonLabel"
defaultMessage="Edit as Query DSL"
/>
)}
</EuiButtonEmpty>
</EuiToolTip>
{this.state.isCustomEditorOpen ? (
<FormattedMessage
id="unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel"
defaultMessage="Edit filter values"
/>
) : (
<FormattedMessage
id="unifiedSearch.filter.filterEditor.editQueryDslButtonLabel"
defaultMessage="Edit as Query DSL"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>