From 3883a47e3ce9328aebd02eb8fbdf9b06b0f7407d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 5 Oct 2022 09:40:04 +0100 Subject: [PATCH] [InMemoryTable] Add `enabled` option to executeQueryOptions (#6284) --- .../basic_table/in_memory_table.test.tsx | 49 +++++++++++++++++++ .../basic_table/in_memory_table.tsx | 12 +++-- src/components/search_bar/query/ast.ts | 36 +++++++++++--- .../search_bar/query/default_syntax.test.ts | 40 +++++++++++++++ upcoming_changelogs/6284.md | 1 + 5 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 upcoming_changelogs/6284.md diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index eb0b5133a7c..182063a441e 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -13,6 +13,7 @@ import { requiredProps } from '../../test'; import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; import { keys, SortDirection } from '../../services'; import { SearchFilterConfig } from '../search_bar/filters'; +import { Query } from '../search_bar/query'; interface BasicItem { id: number | string; @@ -1314,4 +1315,52 @@ describe('EuiInMemoryTable', () => { expect(component.find('td').at(3).text()).toBe('Index3'); }); }); + + describe('controlled search query', () => { + it('execute the Query and filters the table items', () => { + const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }]; + const columns = [{ field: 'title', name: 'Title' }]; + const query = Query.parse('baz'); + + const component = mount( + + ); + + const tableContent = component.find( + '.euiTableRowCell .euiTableCellContent' + ); + + expect(tableContent.length).toBe(1); // only 1 match + expect(tableContent.at(0).text()).toBe('baz'); + }); + + it('does not execute the Query and renders the items passed as is', () => { + const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }]; + const columns = [{ field: 'title', name: 'Title' }]; + const query = Query.parse('baz'); + + const component = mount( + + ); + + const tableContent = component.find( + '.euiTableRowCell .euiTableCellContent' + ); + + expect(tableContent.length).toBe(3); + expect(tableContent.at(0).text()).toBe('foo'); + expect(tableContent.at(1).text()).toBe('bar'); + expect(tableContent.at(2).text()).toBe('baz'); + }); + }); }); diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index ffdd81efd4d..899a873d630 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -86,6 +86,11 @@ type InMemoryTableProps = Omit< defaultFields?: string[]; isClauseMatcher?: (...args: any) => boolean; explain?: boolean; + /** + * When the search bar Query is controlled and passed to the `search` prop it is by default executed against the items passed to the table to filter them out. + * If the filtering is already done before passing the `items` to the table we can disable the execution by setting `enabled` to `false`. + */ + enabled?: boolean; }; /** * Insert content between the search bar and table components. @@ -577,9 +582,10 @@ export class EuiInMemoryTable extends Component< const { query, sortName, pageIndex, pageSize } = this.state; - const matchingItems = query - ? EuiSearchBar.Query.execute(query, items, executeQueryOptions) - : items; + const matchingItems = + query !== null && executeQueryOptions?.enabled !== false + ? EuiSearchBar.Query.execute(query, items, executeQueryOptions) + : items; const sortedItems = sortName ? matchingItems diff --git a/src/components/search_bar/query/ast.ts b/src/components/search_bar/query/ast.ts index 59ecb04f856..55e4990c031 100644 --- a/src/components/search_bar/query/ast.ts +++ b/src/components/search_bar/query/ast.ts @@ -255,6 +255,9 @@ const arrayIncludesValue = (array: any[], value: any) => { return array.some((item) => valuesEqual(item, value)); }; +const mustToMatch = (must: boolean) => + must === true ? Match.MUST : Match.MUST_NOT; + /** * The AST structure is an array of clauses. There are 3 types of clauses that are supported: * @@ -367,13 +370,24 @@ export class _AST { return isNil(value) || arrayIncludesValue(clause.value as Value[], value); } - getOrFieldClause(field: string, value?: Value) { - return this.getFieldClause( - field, - (clause) => - isArray(clause.value) && - (isNil(value) || arrayIncludesValue(clause.value, value)) - ); + getOrFieldClause( + field: string, + value?: Value, + must?: boolean, + operator?: OperatorType + ) { + return this.getFieldClause(field, (clause) => { + if (!isArray(clause.value)) { + return false; + } + + const matchValue = + isNil(value) || arrayIncludesValue(clause.value, value); + const matchMust = isNil(must) || mustToMatch(must) === clause.match; + const matchOperator = isNil(operator) || operator === clause.operator; + + return matchValue && matchMust && matchOperator; + }); } addOrFieldValue( @@ -382,7 +396,13 @@ export class _AST { must = true, operator: OperatorType = Operator.EQ ) { - const existingClause = this.getOrFieldClause(field); + const existingClause = this.getOrFieldClause( + field, + undefined, + must, + operator + ); + if (!existingClause) { const newClause = must ? Field.must[operator](field, [value]) diff --git a/src/components/search_bar/query/default_syntax.test.ts b/src/components/search_bar/query/default_syntax.test.ts index 0114d50582f..0064a45938d 100644 --- a/src/components/search_bar/query/default_syntax.test.ts +++ b/src/components/search_bar/query/default_syntax.test.ts @@ -535,6 +535,46 @@ describe('defaultSyntax', () => { expect(printedQuery).toBe(query); }); + test('field or clause with negation', () => { + const query = 'field:(foo or bar) -field:(baz)'; + const ast = defaultSyntax.parse(query); + + expect(ast).toBeDefined(); + expect(ast.clauses).toHaveLength(2); + + const clauseMust: Clause = ast.getOrFieldClause('field')!; + expect(clauseMust).toBeDefined(); + expect(clauseMust.value).toHaveLength(2); + expect(clauseMust.value).toEqual(['foo', 'bar']); + + const clauseMustNot: Clause = ast.getOrFieldClause( + 'field', + undefined, + false + )!; + expect(clauseMustNot).toBeDefined(); + expect(clauseMustNot.value).toHaveLength(1); + expect(clauseMustNot.value).toEqual(['baz']); + }); + + test('addOrFieldValue()', () => { + const query = 'field:(foo) -field:(baz)'; + let ast = defaultSyntax.parse(query); + expect(ast.getOrFieldClause('field')!.value).toEqual(['foo']); + expect(ast.getOrFieldClause('field', undefined, false)!.value).toEqual([ + 'baz', + ]); + + ast = ast.addOrFieldValue('field', 'foo2'); + ast = ast.addOrFieldValue('field', 'baz2', false); + + expect(ast.getOrFieldClause('field')!.value).toEqual(['foo', 'foo2']); + expect(ast.getOrFieldClause('field', undefined, false)!.value).toEqual([ + 'baz', + 'baz2', + ]); + }); + test('field or & and clause & phrases', () => { const query = 'field1:(foo or "bar baz") -field2:baz'; const ast = defaultSyntax.parse(query); diff --git a/upcoming_changelogs/6284.md b/upcoming_changelogs/6284.md new file mode 100644 index 00000000000..c6638c0c05a --- /dev/null +++ b/upcoming_changelogs/6284.md @@ -0,0 +1 @@ +- Added the `enabled` option to the `` `executeQueryOptions` prop. This option prevents the Query from being executed when controlled by the consumer.