diff --git a/packages/eui/changelogs/upcoming/7960.md b/packages/eui/changelogs/upcoming/7960.md new file mode 100644 index 00000000000..463382867f6 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7960.md @@ -0,0 +1 @@ +- Updated `EuiSearchBar`'s optional `box.schema` prop with a new `recognizedFields` configuration. This allows specifying the phrases that will be parsed as field clauses diff --git a/packages/eui/src/components/search_bar/query/default_syntax.test.ts b/packages/eui/src/components/search_bar/query/default_syntax.test.ts index 1303b93a6e2..9dfa22a7220 100644 --- a/packages/eui/src/components/search_bar/query/default_syntax.test.ts +++ b/packages/eui/src/components/search_bar/query/default_syntax.test.ts @@ -1240,4 +1240,76 @@ describe('defaultSyntax', () => { const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); }); + + describe('recognizedFields', () => { + test('parse field clauses from a query only for recognized fields', () => { + const query = 'remote:test type:dataview happiness'; + const ast = defaultSyntax.parse(query, { + schema: { + recognizedFields: ['tag', 'type'], + }, + }); + expect(ast).toBeDefined(); + expect(ast.clauses).toHaveLength(3); + expect(ast.getFieldClauses()).toMatchObject([ + { field: 'type', match: 'must', value: 'dataview' }, + ]); + expect(ast.getTermClauses()).toEqual([ + { match: 'must', type: 'term', value: 'remote:test' }, + { match: 'must', type: 'term', value: 'happiness' }, + ]); + + const printedQuery = defaultSyntax.print(ast); + expect(printedQuery).toBe('remote\\:test type:dataview happiness'); + }); + + test('combined with "is" clause', () => { + const query = 'remote:test is:pending'; + const ast = defaultSyntax.parse(query, { + schema: { + recognizedFields: ['tag', 'type'], + }, + }); + expect(ast).toBeDefined(); + expect(ast.clauses).toHaveLength(2); + expect(ast.getFieldClauses()).toHaveLength(0); + expect(ast.getIsClauses()).toEqual([ + { flag: 'pending', match: 'must', type: 'is' }, + ]); + expect(ast.getTermClauses()).toEqual([ + { match: 'must', type: 'term', value: 'remote:test' }, + ]); + + const printedQuery = defaultSyntax.print(ast); + expect(printedQuery).toBe('remote\\:test is:pending'); + }); + + test('parse complex terms from a query', () => { + const query = + 'my-remote-cluster:my-remote-index type:data-view tag:really-nice-content'; + const ast = defaultSyntax.parse(query, { + schema: { + recognizedFields: ['tag', 'type'], + }, + }); + expect(ast).toBeDefined(); + expect(ast.clauses).toHaveLength(3); + expect(ast.getFieldClauses()).toMatchObject([ + { field: 'type', match: 'must', value: 'data-view' }, + { field: 'tag', match: 'must', value: 'really-nice-content' }, + ]); + expect(ast.getTermClauses()).toEqual([ + { + match: 'must', + type: 'term', + value: 'my-remote-cluster:my-remote-index', + }, + ]); + + const printedQuery = defaultSyntax.print(ast); + expect(printedQuery).toBe( + 'my\\-remote\\-cluster\\:my\\-remote\\-index type:data-view tag:really-nice-content' + ); + }); + }); }); diff --git a/packages/eui/src/components/search_bar/query/default_syntax.ts b/packages/eui/src/components/search_bar/query/default_syntax.ts index fb1abda17fb..25303b16edd 100644 --- a/packages/eui/src/components/search_bar/query/default_syntax.ts +++ b/packages/eui/src/components/search_bar/query/default_syntax.ts @@ -15,7 +15,8 @@ import peg from 'pegjs-inline-precompile'; // eslint-disable-line import/no-unre const parser = peg` { - const { AST, Exp, unescapeValue, unescapePhraseValue, resolveFieldValue } = options; + const { AST, Exp, unescapeValue, unescapePhraseValue, resolveFieldValue, recognizedFields } = options; + const hasRecognizedFields = recognizedFields && recognizedFields.length > 0; const ctx = Object.assign({ error }, options ); } @@ -108,8 +109,9 @@ FieldLTEValue flagName "flag name" = identifier +// If recognizedFields was given in options, the identifier must match an allowed field fieldName "field name" - = identifier + = id:identifier &{ return !hasRecognizedFields || recognizedFields.includes(id); } { return id; } identifier = identifierChar+ { return unescapeValue(text()); } @@ -256,7 +258,11 @@ interface ValueExpression { export interface ParseOptions { dateFormat?: any; - schema?: any; + schema?: { + strict?: boolean; + fields?: string[] | Record; + recognizedFields?: string[]; + }; escapeValue?: (value: any) => string; } @@ -492,7 +498,7 @@ export const defaultSyntax: Syntax = Object.freeze({ parse: (query: string, options: ParseOptions = {}) => { const dateFormat = options.dateFormat || defaultDateFormat; const parseDate = dateValueParser(dateFormat); - const schema = options.schema || {}; + const { recognizedFields, ...schema } = options.schema || {}; const clauses = parser.parse(query, { AST, Exp, @@ -502,6 +508,7 @@ export const defaultSyntax: Syntax = Object.freeze({ resolveFieldValue, validateFlag, schema: { strict: false, flags: [], fields: {}, ...schema }, + recognizedFields, }); return AST.create(clauses); }, diff --git a/packages/eui/src/components/search_bar/search_bar.tsx b/packages/eui/src/components/search_bar/search_bar.tsx index 046e8eaeb0b..82703a7d739 100644 --- a/packages/eui/src/components/search_bar/search_bar.tsx +++ b/packages/eui/src/components/search_bar/search_bar.tsx @@ -44,6 +44,8 @@ export interface SchemaType { strict?: boolean; fields?: any; flags?: string[]; + // Controls which phrases will be parsed as field clauses + recognizedFields?: string[]; } export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError; @@ -123,7 +125,7 @@ const parseQuery = ( props: EuiSearchBarProps ): Query => { let schema: SchemaType | undefined = undefined; - if (props.box && props.box.schema && typeof props.box.schema === 'object') { + if (props.box?.schema && typeof props.box?.schema === 'object') { schema = props.box.schema; } const dateFormat = props.dateFormat;