From bfe558acf47b28dd238ce77e13805ec4241394ee Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 24 Feb 2020 13:26:27 +0000 Subject: [PATCH 01/10] First pass at TS conversion --- .ignore | 1 + .../basic_table/in_memory_table.test.tsx | 7 +- .../basic_table/in_memory_table.tsx | 151 +------ ....test.js.snap => search_bar.test.tsx.snap} | 0 ....test.js.snap => search_box.test.tsx.snap} | 0 ...t.js.snap => search_filters.test.tsx.snap} | 0 ...ield_value_selection_filter.test.tsx.snap} | 6 +- ...> field_value_toggle_filter.test.tsx.snap} | 0 ...d_value_toggle_group_filter.test.tsx.snap} | 0 ...r.test.js.snap => is_filter.test.tsx.snap} | 0 ... => field_value_selection_filter.test.tsx} | 33 +- ...er.js => field_value_selection_filter.tsx} | 276 ++++++------ ....js => field_value_toggle_filter.test.tsx} | 13 +- ...ilter.js => field_value_toggle_filter.tsx} | 51 +-- ... field_value_toggle_group_filter.test.tsx} | 13 +- ...js => field_value_toggle_group_filter.tsx} | 60 ++- src/components/search_bar/filters/filters.js | 38 -- src/components/search_bar/filters/filters.tsx | 51 +++ src/components/search_bar/filters/index.js | 1 - src/components/search_bar/filters/index.ts | 1 + .../{is_filter.test.js => is_filter.test.tsx} | 4 +- .../filters/{is_filter.js => is_filter.tsx} | 42 +- .../search_bar/{index.js => index.ts} | 2 +- ....snap => ast_to_es_query_dsl.test.ts.snap} | 0 ...ap => ast_to_es_query_string.test.ts.snap} | 0 .../search_bar/query/{ast.js => ast.ts} | 400 +++++++++++------- ...sl.test.js => ast_to_es_query_dsl.test.ts} | 10 +- ...es_query_dsl.js => ast_to_es_query_dsl.ts} | 224 ++++++---- ...test.js => ast_to_es_query_string.test.ts} | 10 +- ...ry_string.js => ast_to_es_query_string.ts} | 84 +++- ...ate_format.test.js => date_format.test.ts} | 2 +- .../query/{date_format.js => date_format.ts} | 95 +++-- ...{date_value.test.js => date_value.test.ts} | 5 +- .../query/{date_value.js => date_value.ts} | 32 +- ..._syntax.test.js => default_syntax.test.ts} | 201 ++++----- .../{default_syntax.js => default_syntax.ts} | 126 ++++-- ...xecute_ast.test.js => execute_ast.test.ts} | 2 +- .../query/{execute_ast.js => execute_ast.ts} | 83 +++- src/components/search_bar/query/index.js | 3 - src/components/search_bar/query/index.ts | 2 + .../{operators.test.js => operators.test.ts} | 14 +- .../query/{operators.js => operators.ts} | 93 ++-- .../search_bar/query/{query.js => query.ts} | 66 +-- ...search_bar.test.js => search_bar.test.tsx} | 29 +- .../{search_bar.js => search_bar.tsx} | 93 ++-- ...search_box.test.js => search_box.test.tsx} | 0 .../{search_box.js => search_box.tsx} | 51 ++- src/components/search_bar/search_filters.js | 37 -- ...ilters.test.js => search_filters.test.tsx} | 30 +- src/components/search_bar/search_filters.tsx | 35 ++ .../table_footer_cell.test.tsx.snap | 16 +- .../table_header_cell.test.tsx.snap | 16 +- .../table_row_cell.test.tsx.snap | 16 +- .../table/table_footer_cell.test.tsx | 33 +- .../table/table_header_cell.test.tsx | 33 +- src/components/table/table_row_cell.test.tsx | 35 +- src/services/predicate/common_predicates.ts | 2 +- 57 files changed, 1511 insertions(+), 1117 deletions(-) create mode 100644 .ignore rename src/components/search_bar/__snapshots__/{search_bar.test.js.snap => search_bar.test.tsx.snap} (100%) rename src/components/search_bar/__snapshots__/{search_box.test.js.snap => search_box.test.tsx.snap} (100%) rename src/components/search_bar/__snapshots__/{search_filters.test.js.snap => search_filters.test.tsx.snap} (100%) rename src/components/search_bar/filters/__snapshots__/{field_value_selection_filter.test.js.snap => field_value_selection_filter.test.tsx.snap} (99%) rename src/components/search_bar/filters/__snapshots__/{field_value_toggle_filter.test.js.snap => field_value_toggle_filter.test.tsx.snap} (100%) rename src/components/search_bar/filters/__snapshots__/{field_value_toggle_group_filter.test.js.snap => field_value_toggle_group_filter.test.tsx.snap} (100%) rename src/components/search_bar/filters/__snapshots__/{is_filter.test.js.snap => is_filter.test.tsx.snap} (100%) rename src/components/search_bar/filters/{field_value_selection_filter.test.js => field_value_selection_filter.test.tsx} (87%) rename src/components/search_bar/filters/{field_value_selection_filter.js => field_value_selection_filter.tsx} (64%) rename src/components/search_bar/filters/{field_value_toggle_filter.test.js => field_value_toggle_filter.test.tsx} (86%) rename src/components/search_bar/filters/{field_value_toggle_filter.js => field_value_toggle_filter.tsx} (55%) rename src/components/search_bar/filters/{field_value_toggle_group_filter.test.js => field_value_toggle_group_filter.test.tsx} (88%) rename src/components/search_bar/filters/{field_value_toggle_group_filter.js => field_value_toggle_group_filter.tsx} (55%) delete mode 100644 src/components/search_bar/filters/filters.js create mode 100644 src/components/search_bar/filters/filters.tsx delete mode 100644 src/components/search_bar/filters/index.js create mode 100644 src/components/search_bar/filters/index.ts rename src/components/search_bar/filters/{is_filter.test.js => is_filter.test.tsx} (84%) rename src/components/search_bar/filters/{is_filter.js => is_filter.tsx} (60%) rename src/components/search_bar/{index.js => index.ts} (81%) rename src/components/search_bar/query/__snapshots__/{ast_to_es_query_dsl.test.js.snap => ast_to_es_query_dsl.test.ts.snap} (100%) rename src/components/search_bar/query/__snapshots__/{ast_to_es_query_string.test.js.snap => ast_to_es_query_string.test.ts.snap} (100%) rename src/components/search_bar/query/{ast.js => ast.ts} (53%) rename src/components/search_bar/query/{ast_to_es_query_dsl.test.js => ast_to_es_query_dsl.test.ts} (93%) rename src/components/search_bar/query/{ast_to_es_query_dsl.js => ast_to_es_query_dsl.ts} (61%) rename src/components/search_bar/query/{ast_to_es_query_string.test.js => ast_to_es_query_string.test.ts} (93%) rename src/components/search_bar/query/{ast_to_es_query_string.js => ast_to_es_query_string.ts} (73%) rename src/components/search_bar/query/{date_format.test.js => date_format.test.ts} (99%) rename src/components/search_bar/query/{date_format.js => date_format.ts} (77%) rename src/components/search_bar/query/{date_value.test.js => date_value.test.ts} (83%) rename src/components/search_bar/query/{date_value.js => date_value.ts} (62%) rename src/components/search_bar/query/{default_syntax.test.js => default_syntax.test.ts} (85%) rename src/components/search_bar/query/{default_syntax.js => default_syntax.ts} (81%) rename src/components/search_bar/query/{execute_ast.test.js => execute_ast.test.ts} (99%) rename src/components/search_bar/query/{execute_ast.js => execute_ast.ts} (76%) delete mode 100644 src/components/search_bar/query/index.js create mode 100644 src/components/search_bar/query/index.ts rename src/components/search_bar/query/{operators.test.js => operators.test.ts} (98%) rename src/components/search_bar/query/{operators.js => operators.ts} (70%) rename src/components/search_bar/query/{query.js => query.ts} (78%) rename src/components/search_bar/{search_bar.test.js => search_bar.test.tsx} (85%) rename src/components/search_bar/{search_bar.js => search_bar.tsx} (65%) rename src/components/search_bar/{search_box.test.js => search_box.test.tsx} (100%) rename src/components/search_bar/{search_box.js => search_box.tsx} (50%) delete mode 100644 src/components/search_bar/search_filters.js rename src/components/search_bar/{search_filters.test.js => search_filters.test.tsx} (65%) create mode 100644 src/components/search_bar/search_filters.tsx diff --git a/.ignore b/.ignore new file mode 100644 index 00000000000..d8f8d46921a --- /dev/null +++ b/.ignore @@ -0,0 +1 @@ +docs diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 798d977024a..3aa378bef45 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -2,13 +2,10 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { requiredProps } from '../../test'; -import { - EuiInMemoryTable, - EuiInMemoryTableProps, - FilterConfig, -} from './in_memory_table'; +import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; import { ENTER } from '../../services/key_codes'; import { SortDirection } from '../../services'; +import { FilterConfig } from '../search_bar/filters'; interface BasicItem { id: number | string; diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index e4d9b803344..ef9091c116f 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -18,134 +18,16 @@ import { } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; -// @ts-ignore -import { EuiSearchBar } from '../search_bar'; +import { EuiSearchBar, Query } from '../search_bar'; import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; - -// Search bar types. Should be moved when it is typescriptified. -interface SearchBoxConfig { - placeholder?: string; - incremental?: boolean; - schema?: SchemaType; -} - -interface SchemaType { - strict?: boolean; - fields?: object; - flags?: string[]; -} - -interface IsFilterConfigType { - type: 'is'; - field: string; - name: string; - negatedName?: string; - available?: () => boolean; -} - -interface FieldValueOptionType { - field?: string; - value: any; - name?: string; - view?: ReactNode; -} - -interface FieldValueSelectionFilterConfigType { - type: 'field_value_selection'; - field?: string; - autoClose?: boolean; - name: string; - options: - | FieldValueOptionType[] - | ((query: Query) => Promise); - filterWith?: - | ((name: string, query: string, options: object) => boolean) - | 'prefix' - | 'includes'; - cache?: number; - multiSelect?: boolean | 'and' | 'or'; - loadingMessage?: string; - noOptionsMessage?: string; - searchThreshold?: number; - available?: () => boolean; -} - -interface FieldValueToggleFilterConfigType { - type: 'field_value_toggle'; - field: string; - value: string | number | boolean; - name: string; - negatedName?: string; - available?: () => boolean; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterItem { - value: string | number | boolean; - name: string; - negatedName?: string; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterConfigType { - type: 'field_value_toggle_group'; - field: string; - items: FieldValueToggleGroupFilterItem[]; - available?: () => boolean; -} - -export type FilterConfig = - | IsFilterConfigType - | FieldValueSelectionFilterConfigType - | FieldValueToggleFilterConfigType - | FieldValueToggleGroupFilterConfigType; - -type SearchBox = Omit & { - schema?: boolean | SchemaType; -}; - -/* Should point at search_bar/query type when it is converted to typescript */ -type Query = any; +import { EuiSearchBarProps } from '../search_bar/search_bar'; +import { SchemaType } from '../search_bar/search_box'; interface onChangeArgument { - query: Query; + query: Query | null; queryText: string; - error: string; -} - -interface EuiSearchBarProps { - /** - The initial query the bar will hold when first mounted - */ - defaultQuery?: Query; - /** - If you wish to use the search bar as a controlled component, continuously pass the query - via this prop - */ - query?: Query; - /** - Configures the search box. Set `placeholder` to change the placeholder text in the box and - `incremental` to support incremental (as you type) search. - */ - box?: SearchBox; - /** - An array of search filters. - */ - filters?: FilterConfig[]; - /** - * Tools which go to the left of the search bar. - */ - toolsLeft?: React.ReactNode; - /** - * Tools which go to the right of the search bar. - */ - toolsRight?: React.ReactNode; - /** - * Date formatter to use when parsing date values - */ - dateFormat?: object; - onChange?: (values: onChangeArgument) => boolean | void; + error: Error | null; } function isEuiSearchBarProps( @@ -208,7 +90,7 @@ interface State { sortName: ReactNode; sortDirection?: Direction; }; - query: Query; + query: Query | null; pageIndex: number; pageSize?: number; pageSizeOptions?: number[]; @@ -219,11 +101,13 @@ interface State { } const getInitialQuery = (search: Search | undefined) => { + let query: Query | string; if (!search) { - return; + query = ''; + } else { + query = (search as EuiSearchBarProps).defaultQuery || ''; } - const query = (search as EuiSearchBarProps).defaultQuery || ''; return isString(query) ? EuiSearchBar.Query.parse(query) : query; }; @@ -382,7 +266,7 @@ export class EuiInMemoryTable extends Component< pageSizeOptions, sortName, sortDirection, - allowNeutralSort: allowNeutralSort === false ? false : true, + allowNeutralSort: allowNeutralSort !== false, hidePerPageOptions, }; } @@ -447,14 +331,11 @@ export class EuiInMemoryTable extends Component< if (isEuiSearchBarProps(this.props.search)) { const search = this.props.search; if (search.onChange) { - const shouldQueryInMemory = search.onChange({ + search.onChange({ query, queryText, error, }); - if (!shouldQueryInMemory) { - return; - } } } @@ -468,7 +349,7 @@ export class EuiInMemoryTable extends Component< renderSearchBar() { const { search } = this.props; if (search) { - let searchBarProps: EuiSearchBarProps = {}; + let searchBarProps: Omit = {}; if (isEuiSearchBarProps(search)) { const { onChange, ..._searchBarProps } = search; @@ -483,7 +364,7 @@ export class EuiInMemoryTable extends Component< } } - resolveSearchSchema() { + resolveSearchSchema(): SchemaType { const { columns } = this.props; return columns.reduce<{ strict: boolean; @@ -501,7 +382,7 @@ export class EuiInMemoryTable extends Component< ); } - getItemSorter() { + getItemSorter(): (a: T, b: T) => number { const { sortName, sortDirection } = this.state; const { columns } = this.props; @@ -512,7 +393,7 @@ export class EuiInMemoryTable extends Component< if (sortColumn == null) { // can't return a non-function so return a function that says everything is the same - return () => () => 0; + return () => 0; } const sortable = sortColumn.sortable; diff --git a/src/components/search_bar/__snapshots__/search_bar.test.js.snap b/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_bar.test.js.snap rename to src/components/search_bar/__snapshots__/search_bar.test.tsx.snap diff --git a/src/components/search_bar/__snapshots__/search_box.test.js.snap b/src/components/search_bar/__snapshots__/search_box.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_box.test.js.snap rename to src/components/search_bar/__snapshots__/search_box.test.tsx.snap diff --git a/src/components/search_bar/__snapshots__/search_filters.test.js.snap b/src/components/search_bar/__snapshots__/search_filters.test.tsx.snap similarity index 100% rename from src/components/search_bar/__snapshots__/search_filters.test.js.snap rename to src/components/search_bar/__snapshots__/search_filters.test.tsx.snap diff --git a/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap b/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap similarity index 99% rename from src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap rename to src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap index 5cf798a14d8..980a2b87d0f 100644 --- a/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.js.snap +++ b/src/components/search_bar/filters/__snapshots__/field_value_selection_filter.test.tsx.snap @@ -252,7 +252,7 @@ exports[`FieldValueSelectionFilter render - all configurations 1`] = ` ownFocus={true} panelClassName="euiFilterGroup__popoverPanel" panelPaddingSize="none" - withTitle={null} + withTitle={false} >
{ test('render - options as a function', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -15,7 +18,7 @@ describe('FieldValueSelectionFilter', () => { type: 'field_value_selection', field: 'tag', name: 'Tag', - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -25,7 +28,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - options as an array', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -57,7 +60,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -91,7 +94,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - all configurations', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -101,11 +104,11 @@ describe('FieldValueSelectionFilter', () => { field: 'tag', name: 'Tag', multiSelect: true, - available: () => {}, + available: () => false, loadingMessage: 'loading...', noOptionsMessage: 'oops...', searchThreshold: 5, - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -115,7 +118,7 @@ describe('FieldValueSelectionFilter', () => { }); test('render - multi-select OR', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -125,11 +128,11 @@ describe('FieldValueSelectionFilter', () => { field: 'tag', name: 'Tag', multiSelect: 'or', - available: () => {}, + available: () => false, loadingMessage: 'loading...', noOptionsMessage: 'oops...', searchThreshold: 5, - options: () => {}, + options: () => Promise.resolve([]), }, }; @@ -139,7 +142,7 @@ describe('FieldValueSelectionFilter', () => { }); test('inactive - field is global', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -171,7 +174,7 @@ describe('FieldValueSelectionFilter', () => { }); test('active - field is global', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -203,7 +206,7 @@ describe('FieldValueSelectionFilter', () => { }); test('inactive - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -237,7 +240,7 @@ describe('FieldValueSelectionFilter', () => { }); test('active - fields in options', () => { - const props = { + const props: FieldValueSelectionFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_selection_filter.js b/src/components/search_bar/filters/field_value_selection_filter.tsx similarity index 64% rename from src/components/search_bar/filters/field_value_selection_filter.js rename to src/components/search_bar/filters/field_value_selection_filter.tsx index d11849d6002..e9a35aed9da 100644 --- a/src/components/search_bar/filters/field_value_selection_filter.js +++ b/src/components/search_bar/filters/field_value_selection_filter.tsx @@ -1,56 +1,52 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactElement, ReactNode } from 'react'; import { isArray, isNil } from '../../../services/predicate'; import { keyCodes } from '../../../services'; -import { EuiPropTypes } from '../../../utils/prop_types'; -import { EuiPopover } from '../../popover/popover'; -import { EuiPopoverTitle } from '../../popover/popover_title'; -import { EuiFieldSearch } from '../../form/field_search/field_search'; -import { EuiFilterSelectItem, EuiFilterButton } from '../../filter_group'; -import { EuiLoadingChart } from '../../loading/loading_chart'; -import { EuiSpacer } from '../../spacer/spacer'; -import { EuiIcon } from '../../icon/icon'; +import { EuiPopover, EuiPopoverTitle } from '../../popover'; +import { EuiFieldSearch } from '../../form/field_search'; +import { EuiFilterButton, EuiFilterSelectItem } from '../../filter_group'; +import { EuiLoadingChart } from '../../loading'; +import { EuiSpacer } from '../../spacer'; +import { EuiIcon } from '../../icon'; import { Query } from '../query'; -const FieldValueOptionType = PropTypes.shape({ - field: PropTypes.string, - value: PropTypes.any.isRequired, - name: PropTypes.string, - view: PropTypes.node, -}); - -const FieldValueOptionsType = PropTypes.oneOfType([ - PropTypes.func, // (query) => Promise - PropTypes.arrayOf(FieldValueOptionType), -]); - -export const FieldValueSelectionFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_selection').isRequired, - field: PropTypes.string, - autoClose: PropTypes.boolean, - name: PropTypes.string.isRequired, - options: FieldValueOptionsType.isRequired, - filterWith: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.oneOf(['prefix', 'includes']), - ]), - cache: PropTypes.number, - multiSelect: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOf(['and', 'or']), - ]), - loadingMessage: PropTypes.string, - noOptionsMessage: PropTypes.string, - searchThreshold: PropTypes.number, - available: PropTypes.func, // () => boolean -}); - -const FieldValueSelectionFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueSelectionFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value) => void -}; +interface FieldValueOptionType { + field?: string; + value: any; + name?: string; + view?: ReactNode; +} + +type OptionsLoader = () => Promise; + +type OptionsFilter = ( + name: string, + query: string, + options?: FieldValueOptionType[] +) => boolean; + +type MultiSelect = boolean | 'and' | 'or'; + +export interface FieldValueSelectionFilterConfigType { + type: 'field_value_selection'; + field?: string; + name: string; + options: FieldValueOptionType[] | OptionsLoader; + filterWith?: 'prefix' | 'includes' | OptionsFilter; + cache?: number; + multiSelect?: MultiSelect; + loadingMessage?: string; + noOptionsMessage?: string; + searchThreshold?: number; + available?: () => boolean; +} + +export interface FieldValueSelectionFilterProps { + index: number; + config: FieldValueSelectionFilterConfigType; + query: Query; + onChange: (value: any) => void; + autoClose?: boolean; +} const defaults = { config: { @@ -62,14 +58,28 @@ const defaults = { }, }; -export class FieldValueSelectionFilter extends Component { - static propTypes = FieldValueSelectionFilterPropTypes; +interface State { + popoverOpen: boolean; + error: string | null; + options: { + all: FieldValueOptionType[]; + shown: FieldValueOptionType[]; + } | null; + cachedOptions?: FieldValueOptionType[] | null; +} - static defaultProps = { +export class FieldValueSelectionFilter extends Component< + FieldValueSelectionFilterProps, + State +> { + static defaultProps: Partial = { autoClose: true, }; - constructor(props) { + private readonly selectItems: EuiFilterSelectItem[]; + private searchInput: HTMLInputElement | null = null; + + constructor(props: FieldValueSelectionFilterProps) { super(props); const { options } = props.config; @@ -136,6 +146,7 @@ export class FieldValueSelectionFilter extends Component { const predicate = this.getOptionFilter(); return { + ...prevState, options: { ...prevState.options, shown: prevState.options.all.filter((option, i, options) => { @@ -148,7 +159,7 @@ export class FieldValueSelectionFilter extends Component { }); } - getOptionFilter() { + getOptionFilter(): OptionsFilter { const filterWith = this.props.config.filterWith || defaults.config.filterWith; @@ -163,42 +174,38 @@ export class FieldValueSelectionFilter extends Component { return (name, query) => name.startsWith(query); } - resolveOptionsLoader() { + resolveOptionsLoader: () => OptionsLoader = () => { const options = this.props.config.options; if (isArray(options)) { return () => Promise.resolve(options); } - if (isNil(this.props.config.cache) || this.props.config.cache <= 0) { - return options; - } + return () => { const cachedOptions = this.state.cachedOptions; if (cachedOptions) { return Promise.resolve(cachedOptions); } - if (this.props.config.cache > 0) { - return new Promise((resolve, reject) => { - return options() - .then(opts => { - this.setState({ cachedOptions: opts }); - this.timeoutId = setTimeout(() => { - this.setState({ cachedOptions: null }); - }, this.props.config.cache); - resolve(opts); - }) - .catch(error => { - reject(error); - }); - }); - } + + return (options as OptionsLoader)().then(opts => { + // If a cache time is set, populate the cache and also schedule a + // cache reset. + if (this.props.config.cache != null && this.props.config.cache > 0) { + this.setState({ cachedOptions: opts }); + setTimeout(() => { + this.setState({ cachedOptions: null }); + }, this.props.config.cache); + } + + return opts; + }); }; - } + }; - resolveOptionName(option) { + resolveOptionName(option: FieldValueOptionType) { return option.name || option.value.toString(); } - onOptionClick(field, value, checked) { + onOptionClick(field: string, value: any, checked: 'on' | 'off' | undefined) { const multiSelect = this.resolveMultiSelect(); const { autoClose } = this.props; @@ -231,7 +238,12 @@ export class FieldValueSelectionFilter extends Component { } } - onKeyDown(index, event) { + onKeyDown( + index: number, + event: + | React.KeyboardEvent + | React.KeyboardEvent + ) { switch (event.keyCode) { case keyCodes.DOWN: if (index < this.selectItems.length - 1) { @@ -254,7 +266,7 @@ export class FieldValueSelectionFilter extends Component { } } - resolveMultiSelect() { + resolveMultiSelect(): MultiSelect { const { config } = this.props; return !isNil(config.multiSelect) ? config.multiSelect @@ -293,7 +305,7 @@ export class FieldValueSelectionFilter extends Component { const threshold = this.props.config.searchThreshold || defaults.config.searchThreshold; const withTitle = - this.state.options && this.state.options.all.length >= threshold; + this.state.options != null && this.state.options.all.length >= threshold; return ( = threshold) { - const disabled = this.state.error; + const disabled = this.state.error != null; return ( { - const optionField = option.field || field; - - const clause = - multiSelect === 'or' - ? query.getOrFieldClause(optionField, option.value) - : query.getSimpleFieldClause(optionField, option.value); - - const checked = this.resolveChecked(clause); - const onClick = () => { - // clicking a checked item will uncheck it and effective remove the filter (value = undefined) - this.onOptionClick(optionField, option.value, checked); - }; - - const item = ( - (this.selectItems[index] = ref)} - onKeyDown={this.onKeyDown.bind(this, index)}> - {option.view ? option.view : this.resolveOptionName(option)} - + + if (this.state.options == null) { + return; + } + + const items: { + on: ReactElement[]; + off: ReactElement[]; + rest: ReactElement[]; + } = { + on: [], + off: [], + rest: [], + }; + + this.state.options.shown.forEach((option, index) => { + const optionField = option.field || field; + + if (optionField == null) { + throw new Error( + 'option.field or field should be provided in ' ); - if (!checked) { - items.rest.push(item); - } else if (checked === 'on') { - items.on.push(item); - } else { - items.off.push(item); - } - return items; - }, - { on: [], off: [], rest: [] } - ); + } + + const clause = + multiSelect === 'or' + ? query.getOrFieldClause(optionField, option.value) + : query.getSimpleFieldClause(optionField, option.value); + + const checked = this.resolveChecked(clause); + const onClick = () => { + // clicking a checked item will uncheck it and effective remove the filter (value = undefined) + this.onOptionClick(optionField, option.value, checked); + }; + + const item = ( + (this.selectItems[index] = ref!)} + onKeyDown={this.onKeyDown.bind(this, index)}> + {option.view ? option.view : this.resolveOptionName(option)} + + ); + + if (!checked) { + items.rest.push(item); + } else if (checked === 'on') { + items.on.push(item); + } else { + items.off.push(item); + } + return items; + }); + return (
{[...items.on, ...items.off, ...items.rest]} @@ -384,7 +421,8 @@ export class FieldValueSelectionFilter extends Component { ); } - resolveChecked(clause) { + // FIXME + resolveChecked(clause: any): 'on' | 'off' | undefined { if (clause) { return Query.isMust(clause) ? 'on' : 'off'; } @@ -404,7 +442,7 @@ export class FieldValueSelectionFilter extends Component { ); } - renderError(message) { + renderError(message: string) { return (
@@ -430,7 +468,7 @@ export class FieldValueSelectionFilter extends Component { ); } - isActiveField(field) { + isActiveField(field: string | undefined): boolean { if (typeof field !== 'string') { return false; } diff --git a/src/components/search_bar/filters/field_value_toggle_filter.test.js b/src/components/search_bar/filters/field_value_toggle_filter.test.tsx similarity index 86% rename from src/components/search_bar/filters/field_value_toggle_filter.test.js rename to src/components/search_bar/filters/field_value_toggle_filter.test.tsx index 04dbf48c568..df045412915 100644 --- a/src/components/search_bar/filters/field_value_toggle_filter.test.js +++ b/src/components/search_bar/filters/field_value_toggle_filter.test.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; import { Query } from '../query'; -import { FieldValueToggleFilter } from './field_value_toggle_filter'; +import { + FieldValueToggleFilter, + FieldValueToggleFilterProps, +} from './field_value_toggle_filter'; describe('FieldValueToggleFilter', () => { test('render', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -24,7 +27,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -42,7 +45,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active negated', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -60,7 +63,7 @@ describe('FieldValueToggleFilter', () => { }); test('render - active negated - custom negated name', () => { - const props = { + const props: FieldValueToggleFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_toggle_filter.js b/src/components/search_bar/filters/field_value_toggle_filter.tsx similarity index 55% rename from src/components/search_bar/filters/field_value_toggle_filter.js rename to src/components/search_bar/filters/field_value_toggle_filter.tsx index 49836c687f2..058f7581cbb 100644 --- a/src/components/search_bar/filters/field_value_toggle_filter.js +++ b/src/components/search_bar/filters/field_value_toggle_filter.tsx @@ -1,39 +1,30 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; import { isNil } from '../../../services/predicate'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; -export const FieldValueToggleFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_toggle').isRequired, - field: PropTypes.string.isRequired, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]).isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - available: PropTypes.func, // () => boolean - operator: PropTypes.oneOf(['eq', 'exact', 'gt', 'gte', 'lt', 'lte']), -}); - -const FieldValueToggleFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueToggleFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; - -export class FieldValueToggleFilter extends Component { - static propTypes = FieldValueToggleFilterPropTypes; +export interface FieldValueToggleFilterConfigType { + type: 'field_value_toggle'; + field: string; + value: string | number | boolean; + name: string; + negatedName?: string; + available?: () => boolean; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} - constructor(props) { - super(props); - } +export interface FieldValueToggleFilterProps { + index: number; + config: FieldValueToggleFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(clause) { +export class FieldValueToggleFilter extends Component< + FieldValueToggleFilterProps +> { + // FIXME + resolveDisplay(clause: any) { const { name, negatedName } = this.props.config; if (isNil(clause)) { return { hasActiveFilters: false, name }; @@ -46,7 +37,7 @@ export class FieldValueToggleFilter extends Component { }; } - valueChanged(checked) { + valueChanged(checked: boolean) { const { field, value, operator } = this.props.config; const query = checked ? this.props.query.removeSimpleFieldValue(field, value) diff --git a/src/components/search_bar/filters/field_value_toggle_group_filter.test.js b/src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx similarity index 88% rename from src/components/search_bar/filters/field_value_toggle_group_filter.test.js rename to src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx index 542f3b12c49..7a859f362ae 100644 --- a/src/components/search_bar/filters/field_value_toggle_group_filter.test.js +++ b/src/components/search_bar/filters/field_value_toggle_group_filter.test.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; import { Query } from '../query'; -import { FieldValueToggleGroupFilter } from './field_value_toggle_group_filter'; +import { + FieldValueToggleGroupFilter, + FieldValueToggleGroupFilterProps, +} from './field_value_toggle_group_filter'; describe('TermToggleGroupFilter', () => { test('render', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -32,7 +35,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -58,7 +61,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active negated', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, @@ -84,7 +87,7 @@ describe('TermToggleGroupFilter', () => { }); test('render - active negated - custom negated name', () => { - const props = { + const props: FieldValueToggleGroupFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/field_value_toggle_group_filter.js b/src/components/search_bar/filters/field_value_toggle_group_filter.tsx similarity index 55% rename from src/components/search_bar/filters/field_value_toggle_group_filter.js rename to src/components/search_bar/filters/field_value_toggle_group_filter.tsx index 6a08fcc1c17..da54e6c7470 100644 --- a/src/components/search_bar/filters/field_value_toggle_group_filter.js +++ b/src/components/search_bar/filters/field_value_toggle_group_filter.tsx @@ -1,42 +1,36 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; -export const FieldValueToggleGroupFilterItemType = PropTypes.shape({ - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]).isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - operator: PropTypes.oneOf(['eq', 'exact', 'gt', 'gte', 'lt', 'lte']), -}); - -export const FieldValueToggleGroupFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('field_value_toggle_group').isRequired, - field: PropTypes.string.isRequired, - items: PropTypes.arrayOf(FieldValueToggleGroupFilterItemType).isRequired, - available: PropTypes.func, // () => boolean -}); - -const FieldValueToggleGroupFilterPropTypes = { - index: PropTypes.number.isRequired, - config: FieldValueToggleGroupFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; +export interface FieldValueToggleGroupFilterItemType { + value: string | number | boolean; + name: string; + negatedName?: string; + operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; +} -export class FieldValueToggleGroupFilter extends Component { - static propTypes = FieldValueToggleGroupFilterPropTypes; +export interface FieldValueToggleGroupFilterConfigType { + type: 'field_value_toggle_group'; + field: string; + items: FieldValueToggleGroupFilterItemType[]; + available?: () => boolean; +} - constructor(props) { - super(props); - } +export interface FieldValueToggleGroupFilterProps { + index: number; + config: FieldValueToggleGroupFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(config, query, item) { +export class FieldValueToggleGroupFilter extends Component< + FieldValueToggleGroupFilterProps +> { + resolveDisplay( + config: FieldValueToggleGroupFilterConfigType, + query: Query, + item: FieldValueToggleGroupFilterItemType + ) { const clause = query.getSimpleFieldClause(config.field, item.value); if (clause) { if (Query.isMust(clause)) { @@ -50,7 +44,7 @@ export class FieldValueToggleGroupFilter extends Component { return { active: false, name: item.name }; } - valueChanged(item, active) { + valueChanged(item: FieldValueToggleGroupFilterItemType, active: boolean) { const { field } = this.props.config; const { value, operator } = item; const query = active diff --git a/src/components/search_bar/filters/filters.js b/src/components/search_bar/filters/filters.js deleted file mode 100644 index 87c09aa7dce..00000000000 --- a/src/components/search_bar/filters/filters.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { IsFilter, IsFilterConfigType } from './is_filter'; -import { - FieldValueSelectionFilter, - FieldValueSelectionFilterConfigType, -} from './field_value_selection_filter'; -import { - FieldValueToggleFilter, - FieldValueToggleFilterConfigType, -} from './field_value_toggle_filter'; -import { - FieldValueToggleGroupFilter, - FieldValueToggleGroupFilterConfigType, -} from './field_value_toggle_group_filter'; -import PropTypes from 'prop-types'; - -export const createFilter = (index, config, query, onChange) => { - const props = { index, config, query, onChange }; - switch (config.type) { - case 'is': - return ; - case 'field_value_selection': - return ; - case 'field_value_toggle': - return ; - case 'field_value_toggle_group': - return ; - default: - throw new Error(`Unknown search filter type [${config.type}]`); - } -}; - -export const FilterConfigType = PropTypes.oneOfType([ - IsFilterConfigType, - FieldValueSelectionFilterConfigType, - FieldValueToggleFilterConfigType, - FieldValueToggleGroupFilterConfigType, -]); diff --git a/src/components/search_bar/filters/filters.tsx b/src/components/search_bar/filters/filters.tsx new file mode 100644 index 00000000000..e473d088455 --- /dev/null +++ b/src/components/search_bar/filters/filters.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { IsFilter, IsFilterConfigType } from './is_filter'; +import { + FieldValueSelectionFilter, + FieldValueSelectionFilterConfigType, +} from './field_value_selection_filter'; +import { + FieldValueToggleFilter, + FieldValueToggleFilterConfigType, +} from './field_value_toggle_filter'; +import { + FieldValueToggleGroupFilter, + FieldValueToggleGroupFilterConfigType, +} from './field_value_toggle_group_filter'; +import { Query } from '../query'; + +export const createFilter = ( + index: number, + config: FilterConfig, + query: Query, + onChange: (query: Query) => void +) => { + const props = { index, query, onChange }; + + // We don't put config in `props` above because TS will give it a wider + // type that we want. Once we've checked `config.type` below, its type + // is narrowed correctly. + switch (config.type) { + case 'is': + return ; + + case 'field_value_selection': + return ; + + case 'field_value_toggle': + return ; + + case 'field_value_toggle_group': + return ; + + default: + // @ts-ignore TS knows that we can't get here + throw new Error(`Unknown search filter type [${config.type}]`); + } +}; + +export type FilterConfig = + | IsFilterConfigType + | FieldValueSelectionFilterConfigType + | FieldValueToggleFilterConfigType + | FieldValueToggleGroupFilterConfigType; diff --git a/src/components/search_bar/filters/index.js b/src/components/search_bar/filters/index.js deleted file mode 100644 index f284d707e29..00000000000 --- a/src/components/search_bar/filters/index.js +++ /dev/null @@ -1 +0,0 @@ -export { createFilter, FilterConfigType } from './filters'; diff --git a/src/components/search_bar/filters/index.ts b/src/components/search_bar/filters/index.ts new file mode 100644 index 00000000000..6dd76a72649 --- /dev/null +++ b/src/components/search_bar/filters/index.ts @@ -0,0 +1 @@ +export { createFilter, FilterConfig } from './filters'; diff --git a/src/components/search_bar/filters/is_filter.test.js b/src/components/search_bar/filters/is_filter.test.tsx similarity index 84% rename from src/components/search_bar/filters/is_filter.test.js rename to src/components/search_bar/filters/is_filter.test.tsx index 0566b6be15f..f560ef6a389 100644 --- a/src/components/search_bar/filters/is_filter.test.js +++ b/src/components/search_bar/filters/is_filter.test.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { requiredProps } from '../../../test'; import { shallow } from 'enzyme'; -import { IsFilter } from './is_filter'; +import { IsFilter, IsFilterProps } from './is_filter'; import { Query } from '../query'; describe('IsFilter', () => { test('render', () => { - const props = { + const props: IsFilterProps = { ...requiredProps, index: 0, onChange: () => {}, diff --git a/src/components/search_bar/filters/is_filter.js b/src/components/search_bar/filters/is_filter.tsx similarity index 60% rename from src/components/search_bar/filters/is_filter.js rename to src/components/search_bar/filters/is_filter.tsx index 304d6f1e2d3..9b0169841d4 100644 --- a/src/components/search_bar/filters/is_filter.js +++ b/src/components/search_bar/filters/is_filter.tsx @@ -1,33 +1,26 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiFilterButton } from '../../filter_group'; import { isNil } from '../../../services/predicate'; -import { EuiPropTypes } from '../../../utils/prop_types'; import { Query } from '../query'; +import { Clause } from '../query/ast'; -export const IsFilterConfigType = PropTypes.shape({ - type: EuiPropTypes.is('is').isRequired, - field: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - negatedName: PropTypes.string, - available: PropTypes.func, // () => boolean -}); - -const IsFilterPropTypes = { - index: PropTypes.number.isRequired, - config: IsFilterConfigType.isRequired, - query: PropTypes.any.isRequired, - onChange: PropTypes.func.isRequired, // (value: boolean) => void -}; - -export class IsFilter extends Component { - static propTypes = IsFilterPropTypes; +export interface IsFilterConfigType { + type: 'is'; + field: string; + name: string; + negatedName?: string; + available?: () => boolean; +} - constructor(props) { - super(props); - } +export interface IsFilterProps { + index: number; + config: IsFilterConfigType; + query: Query; + onChange: (value: Query) => void; +} - resolveDisplay(clause) { +export class IsFilter extends Component { + resolveDisplay(clause: Clause) { const { name, negatedName } = this.props.config; if (isNil(clause)) { return { hasActiveFilters: false, name }; @@ -40,7 +33,8 @@ export class IsFilter extends Component { }; } - valueChanged(field, checked) { + // FIXME + valueChanged(field: any, checked: boolean) { const query = checked ? this.props.query.removeIsClause(field) : this.props.query.addMustIsClause(field); diff --git a/src/components/search_bar/index.js b/src/components/search_bar/index.ts similarity index 81% rename from src/components/search_bar/index.js rename to src/components/search_bar/index.ts index a758d48a4d3..570c9f1e40d 100644 --- a/src/components/search_bar/index.js +++ b/src/components/search_bar/index.ts @@ -1,5 +1,5 @@ export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; -export { SearchBoxConfigPropTypes } from './search_box'; +export { SearchBoxConfigProps } from './search_box'; export { SearchFiltersFiltersType } from './search_filters'; // TODO: Some related types are defined in basic_table/in_memory_table. diff --git a/src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.js.snap b/src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.ts.snap similarity index 100% rename from src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.js.snap rename to src/components/search_bar/query/__snapshots__/ast_to_es_query_dsl.test.ts.snap diff --git a/src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.js.snap b/src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.ts.snap similarity index 100% rename from src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.js.snap rename to src/components/search_bar/query/__snapshots__/ast_to_es_query_string.test.ts.snap diff --git a/src/components/search_bar/query/ast.js b/src/components/search_bar/query/ast.ts similarity index 53% rename from src/components/search_bar/query/ast.js rename to src/components/search_bar/query/ast.ts index 7f8106fd8f2..77837f2eda2 100644 --- a/src/components/search_bar/query/ast.js +++ b/src/components/search_bar/query/ast.ts @@ -1,37 +1,71 @@ import { isArray, isNil } from '../../../services/predicate'; -import { isDateValue, dateValuesEqual } from './date_value'; +import { DateValue, dateValuesEqual, isDateValue } from './date_value'; + +export type MatchType = 'must' | 'must_not'; + +export type Value = string | number | boolean | DateValue; + +export interface IsClause { + type: 'is'; + match?: MatchType; + flag: string; +} + +export interface FieldClause { + type: 'field'; + match?: MatchType; + operator: OperatorType; + field: string; + value: Value | Value[]; +} + +export interface TermClause { + type: 'term'; + match?: MatchType; + value: Value; +} + +export interface GroupClause { + type: 'group'; + match: MatchType; + value: Clause[]; +} + +export type Clause = IsClause | FieldClause | TermClause | GroupClause; export const Match = Object.freeze({ - MUST: 'must', - MUST_NOT: 'must_not', - isMust(match) { + MUST: 'must' as const, + MUST_NOT: 'must_not' as const, + isMust(match: MatchType | undefined) { return match === Match.MUST; }, - isMustClause(clause) { + isMustClause(clause: Clause) { return Match.isMust(clause.match); }, }); +export type OperatorType = 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; + export const Operator = Object.freeze({ - EQ: 'eq', - EXACT: 'exact', - GT: 'gt', - GTE: 'gte', - LT: 'lt', - LTE: 'lte', - isEQ(match) { + EQ: 'eq' as const, + EXACT: 'exact' as const, + GT: 'gt' as const, + GTE: 'gte' as const, + LT: 'lt' as const, + LTE: 'lte' as const, + isEQ(match: OperatorType | undefined) { return match === Operator.EQ; }, - isEQClause(clause) { - return Operator.isEQ(clause.operator); + isEQClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isEQ(clause.operator); }, - isEXACT(match) { + isEXACT(match: OperatorType | undefined) { return match === Operator.EXACT; }, - isEXACTClause(clause) { - return Operator.isEXACT(clause.operator); + isEXACTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isEXACT(clause.operator); }, - isRange(match) { + isRange(match: OperatorType | undefined) { return ( Operator.isGT(match) || Operator.isGTE(match) || @@ -39,103 +73,103 @@ export const Operator = Object.freeze({ Operator.isLTE(match) ); }, - isRangeClause(clause) { - return Operator.isRange(clause.operator); + isRangeClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isRange(clause.operator); }, - isGT(match) { + isGT(match: OperatorType | undefined) { return match === Operator.GT; }, - isGTClause(clause) { - return Operator.isGT(clause.operator); + isGTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isGT(clause.operator); }, - isGTE(match) { + isGTE(match: OperatorType | undefined) { return match === Operator.GTE; }, - isGTEClause(clause) { - return Operator.isGTE(clause.operator); + isGTEClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isGTE(clause.operator); }, - isLT(match) { + isLT(match: OperatorType | undefined) { return match === Operator.LT; }, - isLTClause(clause) { - return Operator.isLT(clause.operator); + isLTClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isLT(clause.operator); }, - isLTE(match) { + isLTE(match: OperatorType | undefined) { return match === Operator.LTE; }, - isLTEClause(clause) { - return Operator.isLTE(clause.operator); + isLTEClause(clause: Clause) { + return Field.isInstance(clause) && Operator.isLTE(clause.operator); }, }); const Term = Object.freeze({ - TYPE: 'term', - isInstance: clause => { + TYPE: 'term' as const, + isInstance: (clause: Clause): clause is TermClause => { return clause.type === Term.TYPE; }, - must: value => { + must: (value: Value) => { return { type: Term.TYPE, value, match: Match.MUST }; }, - mustNot: value => { + mustNot: (value: Value) => { return { type: Term.TYPE, value, match: Match.MUST_NOT }; }, }); const Group = Object.freeze({ - TYPE: 'group', - isInstance: clause => { + TYPE: 'group' as const, + isInstance: (clause: Clause): clause is GroupClause => { return clause.type === Group.TYPE; }, - must: value => { + must: (value: Clause[]) => { return { type: Group.TYPE, value, match: Match.MUST }; }, - mustNot: value => { + mustNot: (value: Clause[]) => { return { type: Group.TYPE, value, match: Match.MUST_NOT }; }, }); const Field = Object.freeze({ - TYPE: 'field', - isInstance: clause => { + TYPE: 'field' as const, + isInstance: (clause: Clause): clause is FieldClause => { return clause.type === Field.TYPE; }, must: { - eq: (field, value) => ({ + eq: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.EQ, }), - exact: (field, value) => ({ + exact: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.EXACT, }), - gt: (field, value) => ({ + gt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.GT, }), - gte: (field, value) => ({ + gte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.GTE, }), - lt: (field, value) => ({ + lt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST, operator: Operator.LT, }), - lte: (field, value) => ({ + lte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, @@ -144,42 +178,42 @@ const Field = Object.freeze({ }), }, mustNot: { - eq: (field, value) => ({ + eq: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.EQ, }), - exact: (field, value) => ({ + exact: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.EXACT, }), - gt: (field, value) => ({ + gt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.GT, }), - gte: (field, value) => ({ + gte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.GTE, }), - lt: (field, value) => ({ + lt: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, match: Match.MUST_NOT, operator: Operator.LT, }), - lte: (field, value) => ({ + lte: (field: string, value: Value | Value[]) => ({ type: Field.TYPE, field, value, @@ -190,26 +224,26 @@ const Field = Object.freeze({ }); const Is = Object.freeze({ - TYPE: 'is', - isInstance: clause => { + TYPE: 'is' as const, + isInstance: (clause: Clause): clause is IsClause => { return clause.type === Is.TYPE; }, - must: flag => { + must: (flag: string) => { return { type: Is.TYPE, flag, match: Match.MUST }; }, - mustNot: flag => { + mustNot: (flag: string) => { return { type: Is.TYPE, flag, match: Match.MUST_NOT }; }, }); -const valuesEqual = (v1, v2) => { +const valuesEqual = (v1: any, v2: any) => { if (isDateValue(v1)) { return dateValuesEqual(v1, v2); } return v1 === v2; }; -const arrayIncludesValue = (array, value) => { +const arrayIncludesValue = (array: any[], value: any) => { return array.some(item => valuesEqual(item, value)); }; @@ -232,36 +266,52 @@ const arrayIncludesValue = (array, value) => { * This AST is immutable - every "mutating" operation returns a newly mutated AST. */ export class _AST { - static create(clauses) { + private readonly _clauses: Clause[]; + private readonly _indexedClauses: { + field: { + [field: string]: FieldClause[]; + }; + is: { + [flag: string]: IsClause; + }; + term: TermClause[]; + group: GroupClause[]; + }; + + static create(clauses: Clause[]) { return new _AST(clauses); } - constructor(clauses = []) { + constructor(clauses: Clause[] = []) { this._clauses = clauses; - this._indexedClauses = clauses.reduce( - (map, clause) => { - switch (clause.type) { - case Field.TYPE: - if (!map.field[clause.field]) { - map.field[clause.field] = []; - } - map.field[clause.field].push(clause); - return map; - case Is.TYPE: - map.is[clause.flag] = clause; - return map; - case Term.TYPE: - map.term.push(clause); - return map; - case Group.TYPE: - map.group.push(clause); - return map; - default: - throw new Error(`Unknown query clause type [${clause.type}]`); - } - }, - { field: {}, is: {}, term: [], group: [] } - ); + this._indexedClauses = { field: {}, is: {}, term: [], group: [] }; + + clauses.forEach(clause => { + switch (clause.type) { + case Field.TYPE: + if (!this._indexedClauses.field[clause.field]) { + this._indexedClauses.field[clause.field] = []; + } + this._indexedClauses.field[clause.field].push(clause); + break; + + case Is.TYPE: + this._indexedClauses.is[clause.flag] = clause; + break; + + case Term.TYPE: + this._indexedClauses.term.push(clause); + break; + + case Group.TYPE: + this._indexedClauses.group.push(clause); + break; + + default: + // @ts-ignore TS knows we have exhausted the match + throw new Error(`Unknown query clause type [${clause.type}]`); + } + }); } get clauses() { @@ -272,7 +322,7 @@ export class _AST { return this._indexedClauses.term; } - getTermClause(value) { + getTermClause(value: Value) { const clauses = this.getTermClauses(); return clauses.find(clause => valuesEqual(clause.value, value)); } @@ -281,31 +331,33 @@ export class _AST { return Object.keys(this._indexedClauses.field); } - getFieldClauses(field = undefined) { + getFieldClauses(field?: string): FieldClause[] { return field ? this._indexedClauses.field[field] : this._clauses.filter(Field.isInstance); } - getFieldClause(field, predicate) { + getFieldClause( + field: string, + predicate: (c: FieldClause) => boolean + ): FieldClause | undefined { const clauses = this.getFieldClauses(field); if (clauses) { return clauses.find(predicate); } } - hasOrFieldClause(field, value = undefined) { - const clauses = this.getFieldClause(field, clause => isArray(clause.value)); - if (!clauses) { + hasOrFieldClause(field: string, value?: Value) { + const clause = this.getFieldClause(field, clause => isArray(clause.value)); + if (!clause) { return false; } - return ( - isNil(value) || - clauses.some(clause => arrayIncludesValue(clause.value, value)) - ); + + // We can apply this type cast due to the filter above + return isNil(value) || arrayIncludesValue(clause.value as Value[], value); } - getOrFieldClause(field, value = undefined) { + getOrFieldClause(field: string, value?: Value) { return this.getFieldClause( field, clause => @@ -314,7 +366,12 @@ export class _AST { ); } - addOrFieldValue(field, value, must = true, operator = Operator.EQ) { + addOrFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const existingClause = this.getOrFieldClause(field); if (!existingClause) { const newClause = must @@ -322,38 +379,45 @@ export class _AST { : Field.mustNot[operator](field, [value]); return new _AST([...this._clauses, newClause]); } + const clauses = this._clauses.map(clause => { if (clause === existingClause) { - clause.value.push(value); + (clause.value as Value[]).push(value); } return clause; }); return new _AST(clauses); } - removeOrFieldValue(field, value) { + removeOrFieldValue(field: string, value: Value) { const existingClause = this.getOrFieldClause(field, value); if (!existingClause) { return new _AST([...this._clauses]); } - const clauses = this._clauses.reduce((clauses, clause) => { - if (clause !== existingClause) { - clauses.push(clause); - return clauses; - } - const filteredValue = clause.value.filter( - val => !valuesEqual(val, value) - ); - if (filteredValue.length === 0) { + const clauses = this._clauses.reduce( + (clauses, clause) => { + if (clause !== existingClause) { + clauses.push(clause); + return clauses; + } + const filteredValue = (clause.value as Value[]).filter( + val => !valuesEqual(val, value) + ); + if (filteredValue.length === 0) { + return clauses; + } + clauses.push({ + ...clause, + value: filteredValue, + }); return clauses; - } - clauses.push({ ...clause, value: filteredValue }); - return clauses; - }, []); + }, + [] as Clause[] + ); return new _AST(clauses); } - removeOrFieldClauses(field) { + removeOrFieldClauses(field: string) { const clauses = this._clauses.filter(clause => { return ( !Field.isInstance(clause) || @@ -364,20 +428,15 @@ export class _AST { return new _AST(clauses); } - hasSimpleFieldClause(field, value = undefined) { - const clauses = this.getFieldClause( - field, - clause => !isArray(clause.value) - ); - if (!clauses) { + hasSimpleFieldClause(field: string, value?: Value) { + const clause = this.getFieldClause(field, clause => !isArray(clause.value)); + if (!clause) { return false; } - return ( - isNil(value) || clauses.some(clause => valuesEqual(clause.value, value)) - ); + return isNil(value) || valuesEqual(clause.value, value); } - getSimpleFieldClause(field, value = undefined) { + getSimpleFieldClause(field: string, value?: Value) { return this.getFieldClause( field, clause => @@ -386,14 +445,19 @@ export class _AST { ); } - addSimpleFieldValue(field, value, must = true, operator = Operator.EQ) { + addSimpleFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const clause = must ? Field.must[operator](field, value) : Field.mustNot[operator](field, value); return this.addClause(clause); } - removeSimpleFieldValue(field, value) { + removeSimpleFieldValue(field: string, value: Value) { const existingClause = this.getSimpleFieldClause(field, value); if (!existingClause) { return new _AST([...this._clauses]); @@ -402,7 +466,7 @@ export class _AST { return new _AST(clauses); } - removeSimpleFieldClauses(field) { + removeSimpleFieldClauses(field: string) { const clauses = this._clauses.filter(clause => { return ( !Field.isInstance(clause) || @@ -417,11 +481,11 @@ export class _AST { return Object.values(this._indexedClauses.is); } - getIsClause(flag) { + getIsClause(flag: string) { return this._indexedClauses.is[flag]; } - removeIsClause(flag) { + removeIsClause(flag: string) { return new _AST( this._clauses.filter( clause => !Is.isInstance(clause) || clause.flag !== flag @@ -452,42 +516,50 @@ export class _AST { * note: in-place replacement means the given clause will be placed in the same position as the one it * replaced */ - addClause(newClause) { + addClause(newClause: Clause) { let added = false; - const newClauses = this._clauses.reduce((clauses, clause) => { - if (newClause.type !== clause.type) { - clauses.push(clause); + const newClauses = this._clauses.reduce( + (clauses, clause) => { + if (newClause.type !== clause.type) { + clauses.push(clause); + return clauses; + } + + switch (newClause.type) { + case Term.TYPE: + if (newClause.value !== (clause as TermClause).value) { + clauses.push(clause); + return clauses; + } + break; + + case Field.TYPE: + if ( + newClause.field !== (clause as FieldClause).field || + newClause.value !== (clause as FieldClause).value + ) { + clauses.push(clause); + return clauses; + } + break; + + case Is.TYPE: + if (newClause.flag !== (clause as IsClause).flag) { + clauses.push(clause); + return clauses; + } + break; + + default: + throw new Error(`unknown clause type [${newClause.type}]`); + } + added = true; + clauses.push(newClause); return clauses; - } - switch (newClause.type) { - case Term.TYPE: - if (newClause.value !== clause.value) { - clauses.push(clause); - return clauses; - } - break; - case Field.TYPE: - if ( - newClause.field !== clause.field || - newClause.value !== clause.value - ) { - clauses.push(clause); - return clauses; - } - break; - case Is.TYPE: - if (newClause.flag !== clause.flag) { - clauses.push(clause); - return clauses; - } - break; - default: - throw new Error(`unknown clause type [${newClause.type}]`); - } - added = true; - clauses.push(newClause); - return clauses; - }, []); + }, + [] as Clause[] + ); + if (!added) { newClauses.push(newClause); } @@ -502,5 +574,5 @@ export const AST = Object.freeze({ Group, Field, Is, - create: clauses => new _AST(clauses), + create: (clauses: Clause[]) => new _AST(clauses), }); diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.test.js b/src/components/search_bar/query/ast_to_es_query_dsl.test.ts similarity index 93% rename from src/components/search_bar/query/ast_to_es_query_dsl.test.js rename to src/components/search_bar/query/ast_to_es_query_dsl.test.ts index dd03a5e6159..a2a5cd9441d 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.test.js +++ b/src/components/search_bar/query/ast_to_es_query_dsl.test.ts @@ -99,7 +99,7 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.gte( 'date', - dateValue(moment.utc('2004-03-22'), Granularity.DAY) + dateValue(moment.utc('2004-03-22'), Granularity.DAY)! ), ]) ); @@ -111,11 +111,11 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.eq( 'date', - dateValue(moment.utc('2004-03'), Granularity.MONTH) + dateValue(moment.utc('2004-03'), Granularity.MONTH)! ), AST.Field.mustNot.lt( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); @@ -127,11 +127,11 @@ describe('astToEsQueryDsl', () => { AST.create([ AST.Field.must.gt( 'date', - dateValue(moment.utc('2004-02'), Granularity.MONTH) + dateValue(moment.utc('2004-02'), Granularity.MONTH)! ), AST.Field.mustNot.gte( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.js b/src/components/search_bar/query/ast_to_es_query_dsl.ts similarity index 61% rename from src/components/search_bar/query/ast_to_es_query_dsl.js rename to src/components/search_bar/query/ast_to_es_query_dsl.ts index acb69606108..8689d7800b2 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.js +++ b/src/components/search_bar/query/ast_to_es_query_dsl.ts @@ -1,9 +1,26 @@ import { printIso8601 } from './date_format'; -import { isDateValue, dateValue } from './date_value'; -import { AST } from './ast'; +import { isDateValue, dateValue, DateValue } from './date_value'; +import { + _AST, + AST, + FieldClause, + IsClause, + OperatorType, + TermClause, + Value, +} from './ast'; import { isArray, isDateLike, isString } from '../../../services/predicate'; -const processDateOperation = (value, operator) => { +interface Options { + defaultFields?: string[]; + extraMustQueries?: any[]; // FIXME + extraMustNotQueries?: any[]; // FIXME + termValuesToQuery?: (terms: Value[], options: {}) => any; + fieldValuesToQuery?: (terms: string, options: {}) => any; + isFlagToQuery?: (flag: string, on: boolean) => any; +} + +const processDateOperation = (value: DateValue, operator?: OperatorType) => { const { granularity, resolve } = value; let expression = printIso8601(resolve()); if (!granularity) { @@ -13,23 +30,27 @@ const processDateOperation = (value, operator) => { case AST.Operator.GT: expression = `${expression}||+1${granularity.es}/${granularity.es}`; return { operator: AST.Operator.GTE, expression }; + case AST.Operator.GTE: expression = `${expression}||/${granularity.es}`; return { operator, expression }; + case AST.Operator.LT: expression = `${expression}||/${granularity.es}`; return { operator, expression }; + case AST.Operator.LTE: expression = `${expression}||+1${granularity.es}/${granularity.es}`; return { operator: AST.Operator.LT, expression }; + default: expression = `${expression}||/${granularity.es}`; return { expression }; } }; -export const _termValuesToQuery = (values, options) => { - const body = { +export const _termValuesToQuery = (values: Value[], options: Options) => { + const body: { query: string; fields?: string[] } = { query: values.join(' '), }; if (body.query === '') { @@ -43,28 +64,32 @@ export const _termValuesToQuery = (values, options) => { }; }; -export const _fieldValuesToQuery = (field, operations, andOr) => { - const queries = []; +export const _fieldValuesToQuery = ( + field: string, + operations: { [x in OperatorType]: Value[] }, + andOr: 'and' | 'or' +) => { + const queries: QueryContainer[] = []; - Object.keys(operations).forEach(operator => { + (Object.keys(operations) as OperatorType[]).forEach(operator => { const values = operations[operator]; switch (operator) { case AST.Operator.EQ: - const { terms, phrases, dates } = values.reduce( - (tokenTypes, value) => { - if (isDateValue(value)) { - tokenTypes.dates.push(value); - } else if (isDateLike(value)) { - tokenTypes.dates.push(dateValue(value)); - } else if (isString(value) && value.match(/\s/)) { - tokenTypes.phrases.push(value); - } else { - tokenTypes.terms.push(value); - } - return tokenTypes; - }, - { terms: [], phrases: [], dates: [] } - ); + const terms: any[] = []; + const phrases: string[] = []; + const dates: DateValue[] = []; + + values.forEach((value: Value) => { + if (isDateValue(value)) { + dates.push(value); + } else if (isDateLike(value)) { + dates.push(dateValue(value)!); + } else if (isString(value) && value.match(/\s/)) { + phrases.push(value); + } else { + terms.push(value); + } + }); if (terms.length > 0) { queries.push({ @@ -100,13 +125,13 @@ export const _fieldValuesToQuery = (field, operations, andOr) => { break; default: - values.forEach(value => { + values.forEach((value: Value) => { if (isDateValue(value)) { const operation = processDateOperation(value, operator); queries.push({ range: { [field]: { - [operation.operator]: operation.expression, + [operation.operator!]: operation.expression, }, }, }); @@ -135,28 +160,26 @@ export const _fieldValuesToQuery = (field, operations, andOr) => { }; }; -export const _isFlagToQuery = (flag, on) => { +export const _isFlagToQuery = (flag: string, on: boolean) => { return { term: { [flag]: on }, }; }; -const collectTerms = clauses => { - return clauses.reduce( - (values, clause) => { - if (AST.Match.isMustClause(clause)) { - values.must.push(clause.value); - } else { - values.mustNot.push(clause.value); - } - return values; - }, - { must: [], mustNot: [] } - ); +const collectTerms = (clauses: TermClause[]) => { + const initialVar: TermsQuery = { must: [], mustNot: [] }; + return clauses.reduce((values, clause) => { + if (AST.Match.isMustClause(clause)) { + values.must.push(clause.value); + } else { + values.mustNot.push(clause.value); + } + return values; + }, initialVar); }; -const collectFields = clauses => { - const fieldArray = (obj, field, operator) => { +const collectFields = (clauses: FieldClause[]) => { + const fieldArray = (obj: any, field: string, operator: OperatorType) => { if (!obj[field]) { obj[field] = {}; } @@ -198,15 +221,23 @@ const collectFields = clauses => { ); }; -const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { +const clausesToEsQueryDsl = ( + { + fields, + terms, + is, + }: { fields: FieldsQuery; terms: TermsQuery; is: IsClause[] }, + options: Options = {} +) => { const extraMustQueries = options.extraMustQueries || []; const extraMustNotQueries = options.extraMustNotQueries || []; const termValuesToQuery = options.termValuesToQuery || _termValuesToQuery; const fieldValuesToQuery = options.fieldValuesToQuery || _fieldValuesToQuery; const isFlagToQuery = options.isFlagToQuery || _isFlagToQuery; - const must = []; + const must: QueryContainer[] = []; must.push(...extraMustQueries); + const termMustQuery = termValuesToQuery(terms.must, options); if (termMustQuery) { must.push(termMustQuery); @@ -234,7 +265,7 @@ const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { mustNot.push(fieldValuesToQuery(field, fields.mustNot.or[field], 'or')); }); - const bool = {}; + const bool: BoolQuery = {}; if (must.length !== 0) { bool.must = must; } @@ -245,13 +276,51 @@ const clausesToEsQueryDsl = ({ fields, terms, is }, options = {}) => { return bool; }; -const EMPTY_TERMS = { must: [], mustNot: [] }; -const EMPTY_FIELDS = { +interface TermsQuery { + must: Value[]; + mustNot: Value[]; +} + +export interface QueryContainer { + bool?: BoolQuery; + match_all?: {}; + match?: object; + match_phrase?: object; + range?: object; +} + +interface BoolQuery { + must?: QueryContainer[]; + must_not?: QueryContainer[]; + should?: QueryContainer[]; +} + +interface FieldsQuery { + must: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; + mustNot: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; +} + +const EMPTY_TERMS: TermsQuery = { must: [], mustNot: [] }; +const EMPTY_FIELDS: FieldsQuery = { must: { and: {}, or: {} }, mustNot: { and: {}, or: {} }, }; -export const astToEsQueryDsl = (ast, options) => { +export const astToEsQueryDsl = (ast: _AST, options = {}): QueryContainer => { if (ast.clauses.length === 0) { return { match_all: {} }; } @@ -271,34 +340,37 @@ export const astToEsQueryDsl = (ast, options) => { // there is at least one GroupClause, wrap the above clauses in another layer and append the ORs const must = groupClauses.reduce( (must, groupClause) => { - const clauses = groupClause.value.reduce((clauses, clause) => { - if (AST.Term.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: collectTerms([clause]), - fields: EMPTY_FIELDS, - is: [], - }) - ); - } else if (AST.Field.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: EMPTY_TERMS, - fields: collectFields([clause]), - is: [], - }) - ); - } else if (AST.Is.isInstance(clause)) { - clauses.push( - clausesToEsQueryDsl({ - terms: EMPTY_TERMS, - fields: EMPTY_FIELDS, - is: [clause], - }) - ); - } - return clauses; - }, []); + const clauses = groupClause.value.reduce( + (clauses, clause) => { + if (AST.Term.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: collectTerms([clause]), + fields: EMPTY_FIELDS, + is: [], + }) + ); + } else if (AST.Field.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: EMPTY_TERMS, + fields: collectFields([clause]), + is: [], + }) + ); + } else if (AST.Is.isInstance(clause)) { + clauses.push( + clausesToEsQueryDsl({ + terms: EMPTY_TERMS, + fields: EMPTY_FIELDS, + is: [clause], + }) + ); + } + return clauses; + }, + [] as BoolQuery[] + ); must.push({ bool: { @@ -308,8 +380,8 @@ export const astToEsQueryDsl = (ast, options) => { return must; }, hasTopMatches // only include the first match group if there are any conditions - ? [{ bool: matchesBool }] - : [] + ? ([{ bool: matchesBool }] as QueryContainer[]) + : ([] as QueryContainer[]) ); return { diff --git a/src/components/search_bar/query/ast_to_es_query_string.test.js b/src/components/search_bar/query/ast_to_es_query_string.test.ts similarity index 93% rename from src/components/search_bar/query/ast_to_es_query_string.test.js rename to src/components/search_bar/query/ast_to_es_query_string.test.ts index 787661b57ef..b95799b1310 100644 --- a/src/components/search_bar/query/ast_to_es_query_string.test.js +++ b/src/components/search_bar/query/ast_to_es_query_string.test.ts @@ -101,7 +101,7 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.gte( 'date', - dateValue(moment.utc('2004-03-22'), Granularity.DAY) + dateValue(moment.utc('2004-03-22'), Granularity.DAY)! ), ]) ); @@ -113,11 +113,11 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.eq( 'date', - dateValue(moment.utc('2004-03'), Granularity.MONTH) + dateValue(moment.utc('2004-03'), Granularity.MONTH)! ), AST.Field.mustNot.lt( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); @@ -129,11 +129,11 @@ describe('astToEsQueryString', () => { AST.create([ AST.Field.must.gt( 'date', - dateValue(moment.utc('2004-02'), Granularity.MONTH) + dateValue(moment.utc('2004-02'), Granularity.MONTH)! ), AST.Field.mustNot.gte( 'date', - dateValue(moment.utc('2004-03-10'), Granularity.DAY) + dateValue(moment.utc('2004-03-10'), Granularity.DAY)! ), ]) ); diff --git a/src/components/search_bar/query/ast_to_es_query_string.js b/src/components/search_bar/query/ast_to_es_query_string.ts similarity index 73% rename from src/components/search_bar/query/ast_to_es_query_string.js rename to src/components/search_bar/query/ast_to_es_query_string.ts index a5ba7b2c5d1..36bed7cec03 100644 --- a/src/components/search_bar/query/ast_to_es_query_string.js +++ b/src/components/search_bar/query/ast_to_es_query_string.ts @@ -1,6 +1,19 @@ +import moment from 'moment'; import { printIso8601 } from './date_format'; -import { isDateValue } from './date_value'; -import { AST, Operator } from './ast'; +import { DateValue, isDateValue } from './date_value'; +import { + _AST, + AST, + Clause, + FieldClause, + GroupClause, + IsClause, + MatchType, + Operator, + OperatorType, + TermClause, + Value, +} from './ast'; import { isArray, isDateLike, @@ -9,14 +22,19 @@ import { isNumber, } from '../../../services/predicate'; -const emitMatch = match => { +const emitMatch = (match: MatchType | undefined) => { if (!match) { return ''; } return AST.Match.isMust(match) ? '+' : '-'; }; -const emitFieldDateLikeClause = (field, value, operator, match) => { +const emitFieldDateLikeClause = ( + field: string, + value: moment.Moment | Date, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); switch (operator) { case Operator.EQ: @@ -34,7 +52,12 @@ const emitFieldDateLikeClause = (field, value, operator, match) => { } }; -const emitFieldDateValueClause = (field, value, operator, match) => { +const emitFieldDateValueClause = ( + field: string, + value: DateValue, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); const { granularity, resolve } = value; const date = resolve(); @@ -67,7 +90,12 @@ const emitFieldDateValueClause = (field, value, operator, match) => { return emitFieldDateLikeClause(field, date, operator, match); }; -const emitFieldNumericClause = (field, value, operator, match) => { +const emitFieldNumericClause = ( + field: string, + value: number, + operator: OperatorType, + match?: MatchType +) => { const matchOp = emitMatch(match); switch (operator) { case Operator.EQ: @@ -85,7 +113,11 @@ const emitFieldNumericClause = (field, value, operator, match) => { } }; -const emitFieldStringClause = (field, value, match) => { +const emitFieldStringClause = ( + field: string, + value: string, + match?: MatchType +) => { const matchOp = emitMatch(match); if (value.match(/\s/)) { return `${matchOp}${field}:"${value}"`; @@ -93,12 +125,21 @@ const emitFieldStringClause = (field, value, match) => { return `${matchOp}${field}:${value}`; }; -const emitFieldBooleanClause = (field, value, match) => { +const emitFieldBooleanClause = ( + field: string, + value: Value, + match?: MatchType +) => { const matchOp = emitMatch(match); return `${matchOp}${field}:${value}`; }; -const emitFieldSingleValueClause = (field, value, operator, match) => { +const emitFieldSingleValueClause = ( + field: string, + value: Value, + operator: OperatorType, + match?: MatchType +) => { if (isDateValue(value)) { return emitFieldDateValueClause(field, value, operator, match); } @@ -117,10 +158,15 @@ const emitFieldSingleValueClause = (field, value, operator, match) => { throw new Error(`unknown type of field value [${value}]`); }; -const emitFieldClause = (clause, isGroupMember) => { +const emitFieldClause = ( + clause: FieldClause, + isGroupMember: boolean +): string => { const { field, value, operator } = clause; let { match } = clause; - if (isGroupMember && AST.Match.isMust(match)) match = null; + if (isGroupMember && AST.Match.isMust(match)) { + match = undefined; + } if (!isArray(value)) { return emitFieldSingleValueClause(field, value, operator, match); @@ -132,23 +178,25 @@ const emitFieldClause = (clause, isGroupMember) => { return `${matchOp}(${clauses})`; }; -const emitTermClause = (clause, isGroupMember) => { +const emitTermClause = (clause: TermClause, isGroupMember: boolean): string => { const { value } = clause; let { match } = clause; - if (isGroupMember && AST.Match.isMust(match)) match = null; + if (isGroupMember && AST.Match.isMust(match)) { + match = undefined; + } const matchOp = emitMatch(match); return `${matchOp}${value}`; }; -const emitIsClause = (clause, isGroupMember) => { +const emitIsClause = (clause: IsClause, isGroupMember: boolean): string => { const { flag, match } = clause; const matchOp = isGroupMember ? '' : '+'; const flagValue = AST.Match.isMust(match); return `${matchOp}${flag}:${flagValue}`; }; -const emitGroupClause = clause => { +const emitGroupClause = (clause: GroupClause): string => { const { value } = clause; const formattedValues = value.map(clause => { return emitClause(clause, true); @@ -156,7 +204,7 @@ const emitGroupClause = clause => { return `+(${formattedValues.join(' ')})`; }; -function emitClause(clause, isGroupMember = false) { +function emitClause(clause: Clause, isGroupMember = false) { if (AST.Field.isInstance(clause)) { return emitFieldClause(clause, isGroupMember); } @@ -167,12 +215,12 @@ function emitClause(clause, isGroupMember = false) { return emitIsClause(clause, isGroupMember); } if (AST.Group.isInstance(clause)) { - return emitGroupClause(clause, isGroupMember); + return emitGroupClause(clause); } throw new Error(`unknown clause type [${JSON.stringify(clause)}]`); } -export const astToEsQueryString = ast => { +export const astToEsQueryString = (ast: _AST) => { if (ast.clauses.length === 0) { return '*'; } diff --git a/src/components/search_bar/query/date_format.test.js b/src/components/search_bar/query/date_format.test.ts similarity index 99% rename from src/components/search_bar/query/date_format.test.js rename to src/components/search_bar/query/date_format.test.ts index 410c66d2164..6e7d64f5bcf 100644 --- a/src/components/search_bar/query/date_format.test.js +++ b/src/components/search_bar/query/date_format.test.ts @@ -1,5 +1,5 @@ import { dateFormat, dateGranularity, Granularity } from './date_format'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; import moment from 'moment'; const random = new Random(); diff --git a/src/components/search_bar/query/date_format.js b/src/components/search_bar/query/date_format.ts similarity index 77% rename from src/components/search_bar/query/date_format.js rename to src/components/search_bar/query/date_format.ts index 097ade9a825..9a1ef96f7d2 100644 --- a/src/components/search_bar/query/date_format.js +++ b/src/components/search_bar/query/date_format.ts @@ -1,12 +1,34 @@ import { dateFormatAliases } from '../../../services/format'; -import moment from 'moment'; +// eslint-disable-next-line import/named +import moment, { Moment, MomentInput } from 'moment'; const utc = moment.utc; const GRANULARITY_KEY = '__eui_granularity'; const FORMAT_KEY = '__eui_format'; -export const Granularity = Object.freeze({ +export interface EuiMoment extends Moment { + __eui_granularity?: GranularityType; + __eui_format?: string; +} + +export interface GranularityType { + es: 'd' | 'w' | 'M' | 'y'; + js: 'day' | 'week' | 'month' | 'year'; + isSame: (d1: Moment, d2: Moment) => boolean; + start: (date: Moment) => Moment; + startOfNext: (date: Moment) => Moment; + iso8601: (date: Moment) => string; +} + +interface GranularitiesType { + DAY: GranularityType; + WEEK: GranularityType; + MONTH: GranularityType; + YEAR: GranularityType; +} + +export const Granularity: GranularitiesType = Object.freeze({ DAY: { es: 'd', js: 'day', @@ -41,26 +63,28 @@ export const Granularity = Object.freeze({ }, }); -const parseTime = value => { - const parsed = utc( +const parseTime = (value: string) => { + const parsed: EuiMoment = utc( value, ['HH:mm', 'H:mm', 'H:mm', 'h:mm a', 'h:mm A', 'hh:mm a', 'hh:mm A'], true ); if (parsed.isValid()) { - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseDay = value => { - let parsed = null; +const parseDay = (value: string) => { + let parsed: EuiMoment; + switch (value.toLowerCase()) { case 'today': parsed = utc().startOf('day'); parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + case 'yesterday': parsed = utc() .subtract(1, 'days') @@ -68,6 +92,7 @@ const parseDay = value => { parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + case 'tomorrow': parsed = utc() .add(1, 'days') @@ -75,6 +100,7 @@ const parseDay = value => { parsed[GRANULARITY_KEY] = Granularity.DAY; parsed[FORMAT_KEY] = value; return parsed; + default: parsed = utc( value, @@ -100,7 +126,7 @@ const parseDay = value => { if (parsed.isValid()) { try { parsed[GRANULARITY_KEY] = Granularity.DAY; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } catch (e) { console.error(e); @@ -109,8 +135,8 @@ const parseDay = value => { } }; -const parseWeek = value => { - let parsed = null; +const parseWeek = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this week': parsed = utc(); @@ -126,18 +152,20 @@ const parseWeek = value => { if (match) { const weekNr = Number(match[1]); parsed = utc().weeks(weekNr); + } else { + return; } } - if (parsed && parsed.isValid()) { + if (parsed != null && parsed.isValid()) { parsed = parsed.startOf('week'); parsed[GRANULARITY_KEY] = Granularity.WEEK; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseMonth = value => { - let parsed = null; +const parseMonth = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this month': parsed = utc(); @@ -156,7 +184,7 @@ const parseMonth = value => { parsed = utc(value, ['MMM', 'MMMM'], true); if (parsed.isValid()) { const now = utc(); - parsed.year(now.year); + parsed.year(now.year()); } else { parsed = utc( value, @@ -176,13 +204,13 @@ const parseMonth = value => { if (parsed.isValid()) { parsed.startOf('month'); parsed[GRANULARITY_KEY] = Granularity.MONTH; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } }; -const parseYear = value => { - let parsed = null; +const parseYear = (value: string) => { + let parsed: EuiMoment; switch (value.toLowerCase()) { case 'this year': parsed = utc().startOf('year'); @@ -209,14 +237,14 @@ const parseYear = value => { parsed = utc(value, ['YY', 'YYYY'], true); if (parsed.isValid()) { parsed[GRANULARITY_KEY] = Granularity.YEAR; - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; return parsed; } } }; -const parseDefault = value => { - let parsed = utc( +const parseDefault = (value: string) => { + let parsed: EuiMoment = utc( value, [ moment.ISO_8601, @@ -237,12 +265,12 @@ const parseDefault = value => { parsed.add(offset, 'minutes'); } if (parsed.isValid()) { - parsed[FORMAT_KEY] = parsed.creationData().format; + parsed[FORMAT_KEY] = parsed.creationData().format as string; } return parsed; }; -const printDay = (now, date, format) => { +const printDay = (now: Moment, date: Moment, format: string) => { if (format.match(/yesterday|tomorrow|today/i)) { if (now.isSame(date, 'day')) { return 'today'; @@ -260,7 +288,7 @@ const printDay = (now, date, format) => { return date.format(format); }; -const printWeek = (now, date, format) => { +const printWeek = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) week/i)) { if (now.isSame(date, 'week')) { return 'This Week'; @@ -285,7 +313,7 @@ const printWeek = (now, date, format) => { return date.format(format); }; -const printMonth = (now, date, format) => { +const printMonth = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) month/i)) { if (now.isSame(date, 'month')) { return 'This Month'; @@ -310,7 +338,7 @@ const printMonth = (now, date, format) => { return date.format(format); }; -const printYear = (now, date, format) => { +const printYear = (now: Moment, date: Moment, format: string) => { if (format.match(/(?:this|next|last) year/i)) { if (now.isSame(date, 'year')) { return 'This Year'; @@ -335,16 +363,16 @@ const printYear = (now, date, format) => { return date.format(format); }; -export const printIso8601 = value => { +export const printIso8601 = (value: MomentInput) => { return utc(value).format(moment.defaultFormatUtc); }; -export const dateGranularity = parsedDate => { - return parsedDate[GRANULARITY_KEY]; +export const dateGranularity = (parsedDate: EuiMoment) => { + return parsedDate[GRANULARITY_KEY]!; }; export const dateFormat = Object.freeze({ - parse(value) { + parse(value: string) { const parsed = parseDay(value) || parseMonth(value) || @@ -358,14 +386,15 @@ export const dateFormat = Object.freeze({ return parsed; }, - print(date, defaultGranularity = undefined) { + print(date: EuiMoment | MomentInput, defaultGranularity = undefined) { date = moment.isMoment(date) ? date : utc(date); + const euiDate: EuiMoment = date as EuiMoment; const now = utc(); - const format = date[FORMAT_KEY]; + const format = euiDate[FORMAT_KEY]; if (!format) { return date.format(dateFormatAliases.iso8601); } - const granularity = date[GRANULARITY_KEY] || defaultGranularity; + const granularity = euiDate[GRANULARITY_KEY] || defaultGranularity; switch (granularity) { case Granularity.DAY: return printDay(now, date, format); diff --git a/src/components/search_bar/query/date_value.test.js b/src/components/search_bar/query/date_value.test.ts similarity index 83% rename from src/components/search_bar/query/date_value.test.js rename to src/components/search_bar/query/date_value.test.ts index 28951e0721e..3ec4be0ad2b 100644 --- a/src/components/search_bar/query/date_value.test.js +++ b/src/components/search_bar/query/date_value.test.ts @@ -1,5 +1,5 @@ import { dateValueParser, isDateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; const random = new Random(); @@ -11,9 +11,10 @@ describe('date value', () => { const format = { parse, print: jest.fn() }; const parser = dateValueParser(format); const value = parser('dateString'); + expect(parse.mock.calls.length).toBe(1); expect(parse.mock.calls[0][0]).toBe('dateString'); expect(isDateValue(value)).toBe(true); - expect(value.resolve().isSame(date)).toBe(true); + expect(value!.resolve().isSame(date)).toBe(true); }); }); diff --git a/src/components/search_bar/query/date_value.js b/src/components/search_bar/query/date_value.ts similarity index 62% rename from src/components/search_bar/query/date_value.js rename to src/components/search_bar/query/date_value.ts index caa714f5a2e..694f0eced74 100644 --- a/src/components/search_bar/query/date_value.js +++ b/src/components/search_bar/query/date_value.ts @@ -2,12 +2,22 @@ import { isDateLike, isNumber } from '../../../services/predicate'; import { dateFormat as defaultDateFormat, dateGranularity, + GranularityType, } from './date_format'; -import moment from 'moment'; +// eslint-disable-next-line import/named +import moment, { MomentInput } from 'moment'; export const DATE_TYPE = 'date'; -export const dateValuesEqual = (v1, v2) => { +export interface DateValue { + type: 'date'; + raw: MomentInput; + granularity: GranularityType | undefined; + text: string; + resolve: () => moment.Moment; +} + +export const dateValuesEqual = (v1: DateValue, v2: DateValue) => { return ( v1.raw === v2.raw && v1.granularity === v2.granularity && @@ -15,7 +25,7 @@ export const dateValuesEqual = (v1, v2) => { ); }; -export const isDateValue = value => { +export const isDateValue = (value: any): value is DateValue => { return ( !!value && value.type === DATE_TYPE && @@ -25,18 +35,28 @@ export const isDateValue = value => { ); }; -export const dateValue = (raw, granularity, dateFormat = defaultDateFormat) => { +export const dateValue: ( + raw: MomentInput, + granularity?: GranularityType, + dateFormat?: any +) => DateValue | undefined = ( + raw, + granularity, + dateFormat = defaultDateFormat +) => { if (!raw) { return undefined; } + if (isDateLike(raw)) { - return { + const dateValue: DateValue = { type: DATE_TYPE, raw, granularity, text: dateFormat.print(raw), resolve: () => moment(raw), }; + return dateValue; } if (isNumber(raw)) { return { @@ -58,7 +78,7 @@ export const dateValue = (raw, granularity, dateFormat = defaultDateFormat) => { }; export const dateValueParser = (format = defaultDateFormat) => { - return text => { + return (text: string) => { const parsed = format.parse(text); return dateValue(text, dateGranularity(parsed), format); }; diff --git a/src/components/search_bar/query/default_syntax.test.js b/src/components/search_bar/query/default_syntax.test.ts similarity index 85% rename from src/components/search_bar/query/default_syntax.test.js rename to src/components/search_bar/query/default_syntax.test.ts index ff5949befda..83ace7d72dc 100644 --- a/src/components/search_bar/query/default_syntax.test.js +++ b/src/components/search_bar/query/default_syntax.test.ts @@ -1,8 +1,8 @@ import { defaultSyntax } from './default_syntax'; -import { AST } from './ast'; +import { AST, Clause, FieldClause } from './ast'; import { Granularity } from './date_format'; -import { isDateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { DateValue, isDateValue } from './date_value'; +import { Random } from '../../../services'; const random = new Random(); @@ -28,39 +28,39 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(6); - let clause = ast.getTermClause('term-1'); + let clause: Clause = ast.getTermClause('term-1')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('term-1'); - clause = ast.getTermClause('term-2'); + clause = ast.getTermClause('term-2')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('term-2'); - clause = ast.getTermClause('-term-3'); + clause = ast.getTermClause('-term-3')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('-term-3'); - clause = ast.getSimpleFieldClause('name-1', 'dash-1'); + clause = ast.getSimpleFieldClause('name-1', 'dash-1')!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name-1'); expect(clause.value).toBe('dash-1'); - clause = ast.getSimpleFieldClause('name-2', 'dash-2'); + clause = ast.getSimpleFieldClause('name-2', 'dash-2')!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name-2'); expect(clause.value).toBe('dash-2'); - clause = ast.getSimpleFieldClause('-name-3', 'dash-3'); + clause = ast.getSimpleFieldClause('-name-3', 'dash-3')!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -76,14 +76,14 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('name', '👸Queen_Elizabeth'); + let clause: Clause = ast.getSimpleFieldClause('name', '👸Queen_Elizabeth')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('👸Queen_Elizabeth'); - clause = ast.getTermClause('🤴King_Henry'); + clause = ast.getTermClause('🤴King_Henry')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -98,25 +98,25 @@ describe('defaultSyntax', () => { expect(ast.clauses).toBeDefined(); expect(ast.clauses).toHaveLength(4); - let clause = ast.getTermClause(':'); + let clause: Clause = ast.getTermClause(':')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe(':'); - clause = ast.getTermClause('\\'); + clause = ast.getTermClause('\\')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('\\'); - clause = ast.getTermClause('('); + clause = ast.getTermClause('(')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('('); - clause = ast.getTermClause(')'); + clause = ast.getTermClause(')')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -133,7 +133,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'john'); + const clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -151,7 +151,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('n:ame', 'jo:h:n'); + const clause: Clause = ast.getSimpleFieldClause('n:ame', 'jo:h:n')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -169,7 +169,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('na_me', 'john'); + const clause: Clause = ast.getSimpleFieldClause('na_me', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -189,7 +189,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'jo-h:n'); + const clause: Clause = ast.getSimpleFieldClause('name', 'jo-h:n')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -207,14 +207,14 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -232,21 +232,21 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(3); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -264,21 +264,21 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(3); - let clause = ast.getSimpleFieldClause('name', 'john'); + let clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6); + clause = ast.getSimpleFieldClause('age', 6)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -296,13 +296,13 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -319,13 +319,13 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -342,33 +342,33 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(5); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getSimpleFieldClause('name', 'john'); + clause = ast.getSimpleFieldClause('name', 'john')!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('bar'); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(5); - clause = ast.getSimpleFieldClause('name', 'joe'); + clause = ast.getSimpleFieldClause('name', 'joe')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -386,46 +386,46 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(7); - let clause = ast.getTermClause('foo'); + let clause: Clause = ast.getTermClause('foo')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getSimpleFieldClause('name', 'john'); + clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getTermClause('bar'); + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('bar'); - clause = ast.getSimpleFieldClause('age', 5); + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(5); - clause = ast.getSimpleFieldClause('name', 'joe'); + clause = ast.getSimpleFieldClause('name', 'joe')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name'); expect(clause.value).toBe('joe'); - clause = ast.getIsClause('open'); + clause = ast.getIsClause('open')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.flag).toBe('open'); - clause = ast.getIsClause('liberal'); + clause = ast.getIsClause('liberal')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -442,7 +442,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause('foo (bar)'); + const clause: Clause = ast.getTermClause('foo (bar)')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -459,7 +459,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('field', 'foo bar'); + const clause: Clause = ast.getSimpleFieldClause('field', 'foo bar')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -478,7 +478,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause(''); + const clause: Clause = ast.getTermClause('')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -495,7 +495,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('field', ''); + const clause: Clause = ast.getSimpleFieldClause('field', '')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -514,7 +514,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getOrFieldClause('field'); + const clause: Clause = ast.getOrFieldClause('field')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -534,7 +534,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getOrFieldClause('field1'); + let clause: Clause = ast.getOrFieldClause('field1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -543,7 +543,7 @@ describe('defaultSyntax', () => { expect(clause.value).toContain('foo'); expect(clause.value).toContain('bar baz'); - clause = ast.getSimpleFieldClause('field2'); + clause = ast.getSimpleFieldClause('field2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -561,7 +561,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('f'); + const clause: Clause = ast.getSimpleFieldClause('f')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -579,7 +579,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getOrFieldClause('f'); + const clause: Clause = ast.getOrFieldClause('f')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -598,17 +598,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isEQClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('12 Jan 2010'); - expect(clause.value.text).toBe('12 Jan 2010'); - expect(clause.value.granularity).toBe(Granularity.DAY); + expect((clause.value as DateValue).raw).toBe('12 Jan 2010'); + expect((clause.value as DateValue).text).toBe('12 Jan 2010'); + expect((clause.value as DateValue).granularity).toBe(Granularity.DAY); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -621,17 +622,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('expires'); + const clause: Clause = ast.getSimpleFieldClause('expires')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isGTClause(clause)).toBe(true); expect(clause.field).toBe('expires'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('last week'); - expect(clause.value.text).toBe('last week'); - expect(clause.value.granularity).toBe(Granularity.WEEK); + expect((clause.value as DateValue).raw).toBe('last week'); + expect((clause.value as DateValue).text).toBe('last week'); + expect((clause.value as DateValue).granularity).toBe(Granularity.WEEK); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -644,17 +646,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('expires'); + const clause: Clause = ast.getSimpleFieldClause('expires')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isGTEClause(clause)).toBe(true); expect(clause.field).toBe('expires'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('next year'); - expect(clause.value.text).toBe('next year'); - expect(clause.value.granularity).toBe(Granularity.YEAR); + expect((clause.value as DateValue).raw).toBe('next year'); + expect((clause.value as DateValue).text).toBe('next year'); + expect((clause.value as DateValue).granularity).toBe(Granularity.YEAR); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -667,17 +670,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isLTClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('last month'); - expect(clause.value.text).toBe('last month'); - expect(clause.value.granularity).toBe(Granularity.MONTH); + expect((clause.value as DateValue).raw).toBe('last month'); + expect((clause.value as DateValue).text).toBe('last month'); + expect((clause.value as DateValue).granularity).toBe(Granularity.MONTH); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -690,17 +694,18 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('created'); + const clause: Clause = ast.getSimpleFieldClause('created')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(AST.Operator.isLTEClause(clause)).toBe(true); expect(clause.field).toBe('created'); expect(clause.value).toBeDefined(); + expect(isDateValue(clause.value)).toBe(true); - expect(clause.value.raw).toBe('Sunday'); - expect(clause.value.text).toBe('Sunday'); - expect(clause.value.granularity).toBe(Granularity.DAY); + expect((clause.value as DateValue).raw).toBe('Sunday'); + expect((clause.value as DateValue).text).toBe('Sunday'); + expect((clause.value as DateValue).granularity).toBe(Granularity.DAY); const printedQuery = defaultSyntax.print(ast); expect(printedQuery).toBe(query); @@ -713,7 +718,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - let clause = ast.getSimpleFieldClause('active'); + let clause: Clause = ast.getSimpleFieldClause('active')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -721,10 +726,10 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('active'); expect(clause.value).toBe(true); - clause = ast.getSimpleFieldClause('closed'); + clause = ast.getSimpleFieldClause('closed')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); - expect(AST.Match.isMust(clause)).toBe(false); + expect(AST.Match.isMustClause(clause)).toBe(false); expect(AST.Operator.isEQClause(clause)).toBe(true); expect(clause.field).toBe('closed'); expect(clause.value).toBe(false); @@ -740,7 +745,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('active'); + const clause: Clause = ast.getSimpleFieldClause('active')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -794,7 +799,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name', 'john'); + const clause: Clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -813,7 +818,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(4); - let clause = ast.getSimpleFieldClause('num1'); + let clause: Clause = ast.getSimpleFieldClause('num1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -821,7 +826,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num1'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('num2'); + clause = ast.getSimpleFieldClause('num2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -829,7 +834,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num2'); expect(clause.value).toBe(8); - clause = ast.getSimpleFieldClause('num3'); + clause = ast.getSimpleFieldClause('num3')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -837,7 +842,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('num3'); expect(clause.value).toBe(4); - clause = ast.getSimpleFieldClause('num4'); + clause = ast.getSimpleFieldClause('num4')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -856,7 +861,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('count'); + const clause: Clause = ast.getSimpleFieldClause('count')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -871,7 +876,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('count'); + const clause: Clause = ast.getSimpleFieldClause('count')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -890,7 +895,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getIsClause('active'); + const clause: Clause = ast.getIsClause('active')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -912,7 +917,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getIsClause('active'); + const clause: Clause = ast.getIsClause('active')!; expect(clause).toBeDefined(); expect(AST.Is.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -925,7 +930,7 @@ describe('defaultSyntax', () => { strict: true, fields: { active: { - type: random.oneOf('number', 'string', 'date'), + type: random.oneOf(['number', 'string', 'date']), }, }, }; @@ -959,7 +964,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name'); + const clause: Clause = ast.getSimpleFieldClause('name')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -1041,7 +1046,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getSimpleFieldClause('name'); + const clause: Clause = ast.getSimpleFieldClause('name')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -1067,7 +1072,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(2); - const ageClause = ast.getSimpleFieldClause('age'); + const ageClause: Clause = ast.getSimpleFieldClause('age')!; expect(ageClause).toBeDefined(); expect(AST.Field.isInstance(ageClause)).toBe(true); expect(AST.Match.isMustClause(ageClause)).toBe(true); @@ -1086,13 +1091,13 @@ describe('defaultSyntax', () => { expect(AST.Field.isInstance(nameClauseA)).toBe(true); expect(AST.Match.isMustClause(nameClauseA)).toBe(true); - expect(nameClauseA.field).toBe('name'); - expect(nameClauseA.value).toBe('john'); + expect((nameClauseA as FieldClause).field).toBe('name'); + expect((nameClauseA as FieldClause).value).toBe('john'); expect(AST.Field.isInstance(nameClauseB)).toBe(true); expect(AST.Match.isMustClause(nameClauseB)).toBe(true); - expect(nameClauseB.field).toBe('name'); - expect(nameClauseB.value).toBe('susan'); + expect((nameClauseB as FieldClause).field).toBe('name'); + expect((nameClauseB as FieldClause).value).toBe('susan'); }); test('negated OR clause', () => { @@ -1122,13 +1127,13 @@ describe('defaultSyntax', () => { expect(AST.Field.isInstance(nameClauseA)).toBe(true); expect(AST.Match.isMustClause(nameClauseA)).toBe(true); - expect(nameClauseA.field).toBe('name'); - expect(nameClauseA.value).toBe('john'); + expect((nameClauseA as FieldClause).field).toBe('name'); + expect((nameClauseA as FieldClause).value).toBe('john'); expect(AST.Field.isInstance(nameClauseB)).toBe(true); expect(AST.Match.isMustClause(nameClauseB)).toBe(true); - expect(nameClauseB.field).toBe('name'); - expect(nameClauseB.value).toBe('susan'); + expect((nameClauseB as FieldClause).field).toBe('name'); + expect((nameClauseB as FieldClause).value).toBe('susan'); }); test('or term parsing and printing', () => { @@ -1138,7 +1143,7 @@ describe('defaultSyntax', () => { expect(ast).toBeDefined(); expect(ast.clauses).toHaveLength(1); - const clause = ast.getTermClause('or'); + const clause: Clause = ast.getTermClause('or')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); diff --git a/src/components/search_bar/query/default_syntax.js b/src/components/search_bar/query/default_syntax.ts similarity index 81% rename from src/components/search_bar/query/default_syntax.js rename to src/components/search_bar/query/default_syntax.ts index 114e174320f..f94634bc860 100644 --- a/src/components/search_bar/query/default_syntax.js +++ b/src/components/search_bar/query/default_syntax.ts @@ -1,7 +1,8 @@ -import { AST } from './ast'; +import { _AST, AST, Clause, OperatorType, Value } from './ast'; import { isArray, isString, isDateLike } from '../../../services/predicate'; import { dateFormat as defaultDateFormat } from './date_format'; -import { dateValueParser, isDateValue } from './date_value'; +import { DateValue, dateValueParser, isDateValue } from './date_value'; +// @ts-ignore import peg from 'pegjs-inline-precompile'; // eslint-disable-line import/no-unresolved const parser = peg` @@ -109,7 +110,7 @@ identifierChar = alnum / [-_] / escapedChar - + fieldRangeValue = rangeValue @@ -124,7 +125,7 @@ containsOrValues = "(" space? head:containsValue tail:( space orWord space value:containsValue { return value; } )* space? ")" { return [ head, ...tail ]; } - + rangeValue = numberWord / date @@ -144,7 +145,7 @@ phrase phraseWord = orWord / word - / [()] // adding parens directly to "wordChar" makes it too aggresive as it consumes the closing paren + / [()] // adding parens directly to "wordChar" makes it too aggresive as it consumes the closing paren word = wordChar+ { @@ -162,7 +163,7 @@ wordChar / [-_*:/] / escapedChar / extendedGlyph - + // This isn't _strictly_ correct: // for our purposes, a non-ascii word character is considered to // be anything above \`Latin-1 Punctuation & Symbols\`, which ends at U+00BF @@ -197,7 +198,7 @@ boolean number = [\\-]?[0-9]+("."[0-9]+)* { return Exp.number(text(), location()); } -// only match numbers followed by whitespace or end of input +// only match numbers followed by whitespace or end of input numberWord = num:number &space { return num; } / num:number !. { return num; } @@ -214,30 +215,78 @@ space "whitespace" = [ \\t\\n\\r]+ `; -const unescapeValue = value => { +type DataType = 'date' | 'number' | 'string' | 'boolean'; + +interface SchemaField { + type: DataType; + valueDescription: string; + validate?: (value: string | boolean | number | DateValue) => void; +} + +interface Context { + schema: { + fields: { + [name: string]: SchemaField; + }; + flags: string[]; + strict: boolean; + }; + strict: boolean; + error: (message: string, location?: Location) => never; + parseDate: (text: string) => DateValue; +} + +type Expression = any; +type Location = any; + +interface ValueExpression { + type: DataType; + expression: string; + location: Location; +} + +export interface Options { + dateFormat?: any; + schema?: any; + escapeValue?: (value: any) => string; +} + +const unescapeValue = (value: string) => { return value.replace(/\\([:\-\\()])/g, '$1'); }; -const escapeValue = value => { +const escapeValue = (value: string) => { return value.replace(/([:\-\\()])/g, '\\$1'); }; -const escapeFieldValue = value => { +const escapeFieldValue = (value: string) => { return value.replace(/(\\)/g, '\\$1'); }; const Exp = { - date: (expression, location) => ({ type: 'date', expression, location }), - number: (expression, location) => ({ type: 'number', expression, location }), - string: (expression, location) => ({ type: 'string', expression, location }), - boolean: (expression, location) => ({ + date: (expression: Expression, location: Location) => ({ + type: 'date', + expression, + location, + }), + number: (expression: Expression, location: Location) => ({ + type: 'number', + expression, + location, + }), + string: (expression: Expression, location: Location) => ({ + type: 'string', + expression, + location, + }), + boolean: (expression: Expression, location: Location) => ({ type: 'boolean', expression, location, }), }; -const validateFlag = (flag, location, ctx) => { +const validateFlag = (flag: string, location: Location, ctx: Context) => { if (ctx.schema && ctx.schema.strict) { if (ctx.schema.flags && ctx.schema.flags.includes(flag)) { return; @@ -254,12 +303,12 @@ const validateFlag = (flag, location, ctx) => { }; const validateFieldValue = ( - field, - schemaField, - expression, - value, - location, - error + field: string, + schemaField: SchemaField, + expression: Expression, + value: Value, + location: Location, + error: Context['error'] ) => { if (schemaField && schemaField.validate) { try { @@ -275,10 +324,17 @@ const validateFieldValue = ( } }; -const resolveFieldValue = (field, valueExpression, ctx) => { +const resolveFieldValue = ( + field: string, + valueExpression: ValueExpression | ValueExpression[], + ctx: Context +): Value | Value[] => { const { schema, error, parseDate } = ctx; if (isArray(valueExpression)) { - return valueExpression.map(exp => resolveFieldValue(field, exp, ctx)); + // FIXME: I don't know if this cast is valid + return valueExpression.map( + exp => resolveFieldValue(field, exp, ctx) as Value + ); } const { location } = valueExpression; let { type, expression } = valueExpression; @@ -301,7 +357,7 @@ const resolveFieldValue = (field, valueExpression, ctx) => { } switch (type) { case 'date': - let date = null; + let date: DateValue | null = null; try { date = parseDate(expression); } catch (e) { @@ -310,8 +366,16 @@ const resolveFieldValue = (field, valueExpression, ctx) => { location ); } - validateFieldValue(field, schemaField, expression, date, location, error); - return date; + // error() throws an exception if called, so now `date` is not null. + validateFieldValue( + field, + schemaField, + expression, + date!, + location, + error + ); + return date!; case 'number': const number = Number(expression); @@ -356,7 +420,7 @@ const resolveFieldValue = (field, valueExpression, ctx) => { } }; -const printValue = (value, options) => { +const printValue = (value: any, options: Options) => { if (isDateValue(value)) { return `'${value.text}'`; } @@ -375,7 +439,7 @@ const printValue = (value, options) => { return escapeFn(value); }; -const resolveOperator = operator => { +const resolveOperator = (operator: OperatorType) => { switch (operator) { case AST.Operator.EQ: return ':'; @@ -395,7 +459,7 @@ const resolveOperator = operator => { }; export const defaultSyntax = Object.freeze({ - parse: (query, options = {}) => { + parse: (query: string, options: Options = {}) => { const dateFormat = options.dateFormat || defaultDateFormat; const parseDate = dateValueParser(dateFormat); const schema = options.schema || {}; @@ -411,7 +475,7 @@ export const defaultSyntax = Object.freeze({ return AST.create(clauses); }, - printClause: (clause, text, options) => { + printClause: (clause: Clause, text: string, options: any): string => { const prefix = AST.Match.isMustClause(clause) ? '' : '-'; switch (clause.type) { case AST.Field.TYPE: @@ -446,7 +510,7 @@ export const defaultSyntax = Object.freeze({ } }, - print: (ast, options = {}) => { + print: (ast: _AST, options = {}) => { return ast.clauses .reduce((text, clause) => { return defaultSyntax.printClause(clause, text, options); diff --git a/src/components/search_bar/query/execute_ast.test.js b/src/components/search_bar/query/execute_ast.test.ts similarity index 99% rename from src/components/search_bar/query/execute_ast.test.js rename to src/components/search_bar/query/execute_ast.test.ts index 5be44c2d11d..11cce9a5c50 100644 --- a/src/components/search_bar/query/execute_ast.test.js +++ b/src/components/search_bar/query/execute_ast.test.ts @@ -1,6 +1,6 @@ import { AST } from './ast'; import { executeAst } from './execute_ast'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; const random = new Random(); diff --git a/src/components/search_bar/query/execute_ast.js b/src/components/search_bar/query/execute_ast.ts similarity index 76% rename from src/components/search_bar/query/execute_ast.js rename to src/components/search_bar/query/execute_ast.ts index cb47597ab05..e2a82cdc824 100644 --- a/src/components/search_bar/query/execute_ast.js +++ b/src/components/search_bar/query/execute_ast.ts @@ -1,7 +1,25 @@ import { get } from '../../../services/objects'; import { isString, isArray } from '../../../services/predicate'; -import { eq, exact, gt, gte, lt, lte } from './operators'; -import { AST } from './ast'; +import { + ClauseValue, + eq, + exact, + FieldValue, + gt, + gte, + lt, + lte, +} from './operators'; +import { + _AST, + AST, + Clause, + FieldClause, + IsClause, + MatchType, + TermClause, + Value, +} from './ast'; const EXPLAIN_FIELD = '__explain'; @@ -14,7 +32,21 @@ const nameToOperatorMap = { [AST.Operator.LTE]: lte, }; -const defaultIsClauseMatcher = (item, clause, explain) => { +interface Explain { + hit: boolean; + type: Clause['type']; + field?: string; + value?: Value | Value[]; + flag?: string; + match?: MatchType; + operator?: any; // FIXME can't be bothered typing this right now +} + +const defaultIsClauseMatcher = ( + item: any, + clause: IsClause, + explain?: Explain[] +) => { const { type, flag, match } = clause; const value = get(item, clause.flag); const must = AST.Match.isMustClause(clause); @@ -25,7 +57,12 @@ const defaultIsClauseMatcher = (item, clause, explain) => { return hit; }; -const fieldClauseMatcher = (item, field, clauses = [], explain) => { +const fieldClauseMatcher = ( + item: any, + field: string, + clauses: FieldClause[] = [], + explain?: Explain[] +) => { return clauses.every(clause => { const { type, value, match } = clause; let operator = nameToOperatorMap[clause.operator]; @@ -34,7 +71,7 @@ const fieldClauseMatcher = (item, field, clauses = [], explain) => { return true; } if (!AST.Match.isMust(match)) { - operator = (value, token) => + operator = (value: FieldValue, token: ClauseValue) => !nameToOperatorMap[clause.operator](value, token); } const itemValue = get(item, field); @@ -48,16 +85,24 @@ const fieldClauseMatcher = (item, field, clauses = [], explain) => { }); }; -const extractStringFieldsFromItem = item => { - return Object.keys(item).reduce((fields, key) => { - if (isString(item[key])) { - fields.push(key); - } - return fields; - }, []); +const extractStringFieldsFromItem = (item: any) => { + return Object.keys(item).reduce( + (fields, key) => { + if (isString(item[key])) { + fields.push(key); + } + return fields; + }, + [] as string[] + ); }; -const termClauseMatcher = (item, fields, clauses = [], explain) => { +const termClauseMatcher = ( + item: any, + fields: string[], + clauses: TermClause[] = [], + explain?: Explain[] +) => { const searchableFields = fields || extractStringFieldsFromItem(item); return clauses.every(clause => { const { type, value, match } = clause; @@ -92,15 +137,15 @@ const termClauseMatcher = (item, fields, clauses = [], explain) => { }; export const createFilter = ( - ast, - defaultFields, + ast: _AST, + defaultFields: string[], isClauseMatcher = defaultIsClauseMatcher, explain = false ) => { // Return items which pass ALL conditions: matches the terms entered, the specified field values, // and the specified "is" clauses. - return item => { - const explainLines = explain ? [] : undefined; + return (item: any) => { + const explainLines = explain ? ([] as Explain[]) : undefined; if (explainLines) { item[EXPLAIN_FIELD] = explainLines; @@ -155,8 +200,8 @@ export const createFilter = ( }; }; -export const executeAst = (ast, items, options = {}) => { +export function executeAst(ast: _AST, items: T[], options: any = {}): T[] { const { isClauseMatcher, defaultFields, explain } = options; const filter = createFilter(ast, defaultFields, isClauseMatcher, explain); return items.filter(filter); -}; +} diff --git a/src/components/search_bar/query/index.js b/src/components/search_bar/query/index.js deleted file mode 100644 index fe0f6519665..00000000000 --- a/src/components/search_bar/query/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { Query } from './query'; -export { AST } from './ast'; -export { dateValueParser as parseDateValue } from './date_value'; diff --git a/src/components/search_bar/query/index.ts b/src/components/search_bar/query/index.ts new file mode 100644 index 00000000000..60e75e781d8 --- /dev/null +++ b/src/components/search_bar/query/index.ts @@ -0,0 +1,2 @@ +export { Query } from './query'; +export { AST } from './ast'; diff --git a/src/components/search_bar/query/operators.test.js b/src/components/search_bar/query/operators.test.ts similarity index 98% rename from src/components/search_bar/query/operators.test.js rename to src/components/search_bar/query/operators.test.ts index 497d2d58057..64f5426ad32 100644 --- a/src/components/search_bar/query/operators.test.js +++ b/src/components/search_bar/query/operators.test.ts @@ -1,18 +1,26 @@ import moment from 'moment'; import { eq, gt, gte, lt, lte } from './operators'; import { dateValue } from './date_value'; -import { Random } from '../../../services/random'; +import { Random } from '../../../services'; import { Granularity } from './date_format'; const random = new Random(); -const laterMoment = (date, count, units) => { +const laterMoment = ( + date: moment.MomentInput, + count: number, + units: 'hours' | 'days' | 'weeks' | 'months' | 'years' +) => { const later = moment(date); later.add(count, units); return later; }; -const earlierMoment = (date, count, units) => { +const earlierMoment = ( + date: moment.MomentInput, + count: number, + units: 'hours' | 'days' | 'weeks' | 'months' | 'years' +) => { const later = moment(date); later.subtract(count, units); return later; diff --git a/src/components/search_bar/query/operators.js b/src/components/search_bar/query/operators.ts similarity index 70% rename from src/components/search_bar/query/operators.js rename to src/components/search_bar/query/operators.ts index d7ae3df4ab7..e3680f608be 100644 --- a/src/components/search_bar/query/operators.js +++ b/src/components/search_bar/query/operators.ts @@ -9,29 +9,64 @@ import { isNil, } from '../../../services/predicate'; import moment from 'moment'; +import { Value } from './ast'; + +export type FieldValue = + | string + | number + | boolean + | any[] + | Date + | moment.Moment + | null + | undefined; + +export type ClauseValue = Value | Date | moment.Moment | null | undefined; + const utc = moment.utc; -const resolveValueAsDate = value => { +const resolveValueAsDate = (value: FieldValue) => { if (moment.isMoment(value)) { return value; } if (moment.isDate(value) || isNumber(value)) { return moment(value); } - return dateFormat.parse(value.toString()); + return dateFormat.parse((value || '').toString()); }; -const defaultEqOptions = { +type Options = Partial<{ + exactMatch: boolean; + ignoreCase: boolean; +}>; + +const defaultEqOptions: Options = { ignoreCase: true, }; -export const eq = (fieldValue, clauseValue, options = {}) => { +export const eq = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + options: Options = {} +): boolean => { options = { ...defaultEqOptions, ...options }; if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } + if (isBoolean(fieldValue)) { + return clauseValue === fieldValue; + } + + if (isArray(fieldValue)) { + if (fieldValue.length > 0) { + return fieldValue.some(item => eq(item, clauseValue, options)); + } else { + return eq('', clauseValue, options); + } + } + if (isDateValue(clauseValue)) { const dateFieldValue = resolveValueAsDate(fieldValue); if (clauseValue.granularity) { @@ -62,10 +97,6 @@ export const eq = (fieldValue, clauseValue, options = {}) => { return fieldValue === clauseValue; } - if (isBoolean(fieldValue)) { - return clauseValue === fieldValue; - } - if (isDateLike(fieldValue)) { const date = resolveValueAsDate(clauseValue); if (!date.isValid()) { @@ -75,44 +106,48 @@ export const eq = (fieldValue, clauseValue, options = {}) => { if (!granularity) { return utc(fieldValue).isSame(date); } - return granularity.isSame(fieldValue, date); - } - - if (isArray(fieldValue)) { - if (fieldValue.length > 0) { - return fieldValue.some(item => eq(item, clauseValue, options)); - } else { - return eq('', clauseValue, options); - } + return granularity.isSame(fieldValue as moment.Moment, date); } return false; // unknown value type }; -export const exact = (fieldValue, clauseValue, options = {}) => { +export const exact = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + options = {} +) => { return eq(fieldValue, clauseValue, { ...options, exactMatch: true }); }; -const greaterThen = (fieldValue, clauseValue, inclusive = false) => { +const greaterThen = ( + fieldValue: FieldValue, + clauseValue: ClauseValue, + inclusive = false +): boolean => { if (isDateValue(clauseValue)) { const clauseDateValue = clauseValue.resolve(); + + const fieldValueAsMomentInput = fieldValue as moment.MomentInput; + if (!clauseValue.granularity) { return inclusive - ? utc(fieldValue).isSameOrAfter(clauseDateValue) - : utc(fieldValue).isAfter(clauseDateValue); + ? utc(fieldValueAsMomentInput).isSameOrAfter(clauseDateValue) + : utc(fieldValueAsMomentInput).isAfter(clauseDateValue); } + if (inclusive) { - return utc(fieldValue).isSameOrAfter( + return utc(fieldValueAsMomentInput).isSameOrAfter( clauseValue.granularity.start(clauseDateValue) ); } - return utc(fieldValue).isSameOrAfter( + return utc(fieldValueAsMomentInput).isSameOrAfter( clauseValue.granularity.startOfNext(clauseDateValue) ); } if (isString(fieldValue)) { - const str = clauseValue.toString(); + const str = clauseValue ? clauseValue.toString() : ''; return inclusive ? fieldValue >= str : fieldValue > str; } @@ -136,34 +171,34 @@ const greaterThen = (fieldValue, clauseValue, inclusive = false) => { } if (isArray(fieldValue)) { - return fieldValue.all(item => greaterThen(item, clauseValue, inclusive)); + return fieldValue.every(item => greaterThen(item, clauseValue, inclusive)); } return false; // unsupported value type }; -export const gt = (fieldValue, clauseValue) => { +export const gt = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return false; } return greaterThen(fieldValue, clauseValue); }; -export const gte = (fieldValue, clauseValue) => { +export const gte = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } return greaterThen(fieldValue, clauseValue, true); }; -export const lt = (fieldValue, clauseValue) => { +export const lt = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return false; } return !greaterThen(fieldValue, clauseValue, true); }; -export const lte = (fieldValue, clauseValue) => { +export const lte = (fieldValue: FieldValue, clauseValue: ClauseValue) => { if (isNil(fieldValue) || isNil(clauseValue)) { return fieldValue === clauseValue; } diff --git a/src/components/search_bar/query/query.js b/src/components/search_bar/query/query.ts similarity index 78% rename from src/components/search_bar/query/query.js rename to src/components/search_bar/query/query.ts index b9f09c225ff..05c8de8f432 100644 --- a/src/components/search_bar/query/query.js +++ b/src/components/search_bar/query/query.ts @@ -3,8 +3,7 @@ import { executeAst } from './execute_ast'; import { isNil, isString } from '../../../services/predicate'; import { astToEsQueryDsl } from './ast_to_es_query_dsl'; import { astToEsQueryString } from './ast_to_es_query_string'; -import { dateValueParser } from './date_value'; -import { AST, Operator } from './ast'; +import { _AST, AST, Clause, Operator, OperatorType, Value } from './ast'; /** * This is the consumer interface for the query - it's effectively a wrapper construct around @@ -12,103 +11,108 @@ import { AST, Operator } from './ast'; * It is immutable - all mutating operations return a new (mutated) query instance. */ export class Query { - static parse(text, options, syntax = defaultSyntax) { + static parse(text: string, options?: {}, syntax = defaultSyntax) { return new Query(syntax.parse(text, options), syntax, text); } - static parseDateValue(value, format = undefined) { - return dateValueParser(format)(value); - } - - static isMust(clause) { + static isMust(clause: Clause) { return AST.Match.isMustClause(clause); } static MATCH_ALL = Query.parse(''); - static isTerm(clause) { + static isTerm(clause: Clause) { return AST.Term.isInstance(clause); } - static isIs(clause) { + static isIs(clause: Clause) { return AST.Is.isInstance(clause); } - static isField(clause) { + static isField(clause: Clause) { return AST.Field.isInstance(clause); } - constructor(ast, syntax = defaultSyntax, text = undefined) { + private ast: _AST; + public text: string; + private syntax: any; + + constructor(ast: _AST, syntax = defaultSyntax, text?: string) { this.ast = ast; this.text = text || syntax.print(ast); this.syntax = syntax; } - hasSimpleFieldClause(field, value = undefined) { + hasSimpleFieldClause(field: string, value?: string) { return this.ast.hasSimpleFieldClause(field, value); } - getSimpleFieldClause(field, value) { + getSimpleFieldClause(field: string, value?: Value) { return this.ast.getSimpleFieldClause(field, value); } - removeSimpleFieldClauses(field) { + removeSimpleFieldClauses(field: string) { const ast = this.ast.removeSimpleFieldClauses(field); return new Query(ast, this.syntax); } - addSimpleFieldValue(field, value, must = true, operator = Operator.EQ) { + addSimpleFieldValue( + field: string, + value: Value, + must = true, + operator: OperatorType = Operator.EQ + ) { const ast = this.ast.addSimpleFieldValue(field, value, must, operator); return new Query(ast, this.syntax); } - removeSimpleFieldValue(field, value) { + removeSimpleFieldValue(field: string, value: Value) { const ast = this.ast.removeSimpleFieldValue(field, value); return new Query(ast, this.syntax); } - hasOrFieldClause(field, value = undefined) { + hasOrFieldClause(field: string, value?: Value) { return this.ast.hasOrFieldClause(field, value); } - getOrFieldClause(field, value) { + getOrFieldClause(field: string, value?: Value) { return this.ast.getOrFieldClause(field, value); } - addOrFieldValue(field, value, must = true) { + addOrFieldValue(field: string, value: Value, must = true) { const ast = this.ast.addOrFieldValue(field, value, must); return new Query(ast, this.syntax); } - removeOrFieldValue(field, value) { + removeOrFieldValue(field: string, value: Value) { const ast = this.ast.removeOrFieldValue(field, value); return new Query(ast, this.syntax); } - removeOrFieldClauses(field) { + removeOrFieldClauses(field: string) { const ast = this.ast.removeOrFieldClauses(field); return new Query(ast, this.syntax); } - hasIsClause(flag) { + hasIsClause(flag: string) { return !isNil(this.ast.getIsClause(flag)); } - getIsClause(flag) { + getIsClause(flag: string) { return this.ast.getIsClause(flag); } - addMustIsClause(flag) { + addMustIsClause(flag: string) { const ast = this.ast.addClause(AST.Is.must(flag)); return new Query(ast, this.syntax); } - addMustNotIsClause(flag) { + addMustNotIsClause(flag: string) { const ast = this.ast.addClause(AST.Is.mustNot(flag)); return new Query(ast, this.syntax); } - removeIsClause(flag) { + removeIsClause(flag: string) { const ast = this.ast.removeIsClause(flag); return new Query(ast, this.syntax); } @@ -135,7 +139,7 @@ export class Query { * information about why the objects matched the query (default to `false`, mainly/only useful for * debugging) */ - static execute(query, items, options = {}) { + static execute(query: string | Query, items: T[], options = {}): T[] { const q = isString(query) ? Query.parse(query) : query; return executeAst(q.ast, items, options); } @@ -166,13 +170,13 @@ export class Query { * terms in the query(the operator is AND). This function lets you change this default translation * and provide your own custom one. */ - static toESQuery(query, options = {}) { + static toESQuery(query: string | Query, options = {}) { const q = isString(query) ? Query.parse(query) : query; return astToEsQueryDsl(q.ast, options); } - static toESQueryString(query, options = {}) { + static toESQueryString(query: string | Query) { const q = isString(query) ? Query.parse(query) : query; - return astToEsQueryString(q.ast, options); + return astToEsQueryString(q.ast); } } diff --git a/src/components/search_bar/search_bar.test.js b/src/components/search_bar/search_bar.test.tsx similarity index 85% rename from src/components/search_bar/search_bar.test.js rename to src/components/search_bar/search_bar.test.tsx index 4581fb60eaa..f783e07c4a1 100644 --- a/src/components/search_bar/search_bar.test.js +++ b/src/components/search_bar/search_bar.test.tsx @@ -5,6 +5,7 @@ import { mount, shallow } from 'enzyme'; import { EuiSearchBar } from './search_bar'; import { Query } from './query'; import { ENTER } from '../../services/key_codes'; +import { SearchFiltersFiltersType } from './search_filters'; describe('SearchBar', () => { test('render - no config, no query', () => { @@ -47,21 +48,23 @@ describe('SearchBar', () => { }); test('render - provided query, filters', () => { + const filters: SearchFiltersFiltersType = [ + { + type: 'is', + field: 'open', + name: 'Open', + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + options: () => Promise.resolve([]), + }, + ]; + const props = { ...requiredProps, - filters: [ - { - type: 'is', - field: 'open', - name: 'Open', - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: () => {}, - }, - ], + filters, query: 'this is a query', onChange: () => {}, }; diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.tsx similarity index 65% rename from src/components/search_bar/search_bar.js rename to src/components/search_bar/search_bar.tsx index cd214f118b6..ba2370e63c9 100644 --- a/src/components/search_bar/search_bar.js +++ b/src/components/search_bar/search_bar.tsx @@ -1,64 +1,66 @@ -import React, { Component } from 'react'; +import React, { Component, ReactElement } from 'react'; import { isString } from '../../services/predicate'; -import { EuiFlexGroup } from '../flex/flex_group'; -import { EuiSearchBox, SearchBoxConfigPropTypes } from './search_box'; +import { EuiFlexGroup, EuiFlexItem } from '../flex'; +import { EuiSearchBox, SearchBoxConfigProps } from './search_box'; import { EuiSearchFilters, SearchFiltersFiltersType } from './search_filters'; -import PropTypes from 'prop-types'; import { Query } from './query'; -import { EuiFlexItem } from '../flex/flex_item'; +import { CommonProps } from '../common'; export { Query, AST as Ast } from './query'; -export const QueryType = PropTypes.oneOfType([ - PropTypes.instanceOf(Query), - PropTypes.string, -]); +export type QueryType = Query | string; -export const SearchBarPropTypes = { - /** - (query?: Query, queryText: string, error?: string) => void - */ - onChange: PropTypes.func.isRequired, +type Tools = ReactElement | ReactElement[]; + +export interface EuiSearchBarProps extends CommonProps { + onChange?: (args: { + query: Query | null; + queryText: string; + error: Error | null; + }) => void; /** The initial query the bar will hold when first mounted */ - defaultQuery: QueryType, + defaultQuery?: QueryType; /** If you wish to use the search bar as a controlled component, continuously pass the query via this prop */ - query: QueryType, + query?: QueryType; /** Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search. */ - box: PropTypes.shape(SearchBoxConfigPropTypes), + box?: SearchBoxConfigProps; /** An array of search filters. */ - filters: SearchFiltersFiltersType, + filters?: SearchFiltersFiltersType; /** * Tools which go to the left of the search bar. */ - toolsLeft: PropTypes.node, + toolsLeft?: Tools; /** * Tools which go to the right of the search bar. */ - toolsRight: PropTypes.node, + toolsRight?: Tools; /** * Date formatter to use when parsing date values */ - dateFormat: PropTypes.object, -}; + dateFormat?: object; +} -const parseQuery = (query, props) => { +const parseQuery = ( + query: QueryType | undefined, + props: EuiSearchBarProps +): Query => { const schema = props.box ? props.box.schema : undefined; const dateFormat = props.dateFormat; const parseOptions = { schema, dateFormat }; @@ -68,12 +70,18 @@ const parseQuery = (query, props) => { return isString(query) ? Query.parse(query, parseOptions) : query; }; -export class EuiSearchBar extends Component { - static propTypes = SearchBarPropTypes; +interface State { + query: Query; + queryText: string; + error: null | Error; +} + +type StateWithOptionalQuery = Omit & { query: Query | null }; +export class EuiSearchBar extends Component { static Query = Query; - constructor(props) { + constructor(props: EuiSearchBarProps) { super(props); const query = parseQuery(props.defaultQuery || props.query, props); this.state = { @@ -83,10 +91,17 @@ export class EuiSearchBar extends Component { }; } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps( + nextProps: EuiSearchBarProps, + prevState: State + ): State | null { if ( nextProps.query && - (!prevState.query || nextProps.query.text !== prevState.query.text) + (!prevState.query || + (typeof nextProps.query !== 'string' && + nextProps.query.text !== prevState.query.text) || + (typeof nextProps.query === 'string' && + nextProps.query !== prevState.query.text)) ) { const query = parseQuery(nextProps.query, nextProps); return { @@ -98,7 +113,11 @@ export class EuiSearchBar extends Component { return null; } - notifyControllingParent(newState) { + notifyControllingParent(newState: StateWithOptionalQuery) { + const { onChange } = this.props; + if (!onChange) { + return; + } const oldState = this.state; const { query, queryText, error } = newState; @@ -109,23 +128,23 @@ export class EuiSearchBar extends Component { const isErrorDifferent = oldError !== newError; if (isQueryDifferent || isErrorDifferent) { - this.props.onChange({ query, queryText, error }); + onChange({ query, queryText, error }); } } - onSearch = queryText => { + onSearch = (queryText: string) => { try { const query = parseQuery(queryText, this.props); this.notifyControllingParent({ query, queryText, error: null }); this.setState({ query, queryText, error: null }); } catch (e) { - const error = { message: e.message }; + const error: Error = { name: e.name, message: e.message }; this.notifyControllingParent({ query: null, queryText, error }); this.setState({ queryText, error }); } }; - onFiltersChange = query => { + onFiltersChange = (query: Query) => { this.notifyControllingParent({ query, queryText: query.text, error: null }); this.setState({ query, @@ -134,14 +153,16 @@ export class EuiSearchBar extends Component { }); }; - renderTools(tools) { + renderTools(tools?: Tools) { if (!tools) { return undefined; } if (Array.isArray(tools)) { return tools.map(tool => ( - + // There's a mismatch somewhere around how the key attribute / + // property is defined, such that `null` is not allowed + {tool} )); @@ -178,7 +199,7 @@ export class EuiSearchBar extends Component { {...box} query={queryText} onSearch={this.onSearch} - isInvalid={!!error} + isInvalid={error != null} title={error ? error.message : undefined} /> diff --git a/src/components/search_bar/search_box.test.js b/src/components/search_bar/search_box.test.tsx similarity index 100% rename from src/components/search_bar/search_box.test.js rename to src/components/search_bar/search_box.test.tsx diff --git a/src/components/search_bar/search_box.js b/src/components/search_bar/search_box.tsx similarity index 50% rename from src/components/search_bar/search_box.js rename to src/components/search_bar/search_box.tsx index 5e41ef6d0af..4805331f027 100644 --- a/src/components/search_bar/search_box.js +++ b/src/components/search_bar/search_box.tsx @@ -1,39 +1,38 @@ import React, { Component } from 'react'; -import { EuiFieldSearch } from '../form/field_search/field_search'; -import PropTypes from 'prop-types'; +import { EuiFieldSearch } from '../form/field_search'; +import { CommonProps } from '../common'; -export const SchemaType = PropTypes.shape({ - strict: PropTypes.bool, - fields: PropTypes.object, - flags: PropTypes.arrayOf(PropTypes.string), -}); +export interface SchemaType { + strict: boolean; + fields: any; + flags?: string[]; +} -export const SearchBoxConfigPropTypes = { - placeholder: PropTypes.string, - incremental: PropTypes.bool, - schema: SchemaType, -}; +export interface SearchBoxConfigProps extends CommonProps { + placeholder?: string; + incremental?: boolean; + // Boolean values are not meaningful to this component, but are allowed so that other + // components can use e.g. a true value to mean "auto-derive a schema". See EuiInMemoryTable. + schema?: SchemaType | boolean; +} -export class EuiSearchBox extends Component { - static propTypes = { - query: PropTypes.string.isRequired, - onSearch: PropTypes.func.isRequired, // (queryText) => void - isInvalid: PropTypes.bool, - title: PropTypes.string, - ...SearchBoxConfigPropTypes, - }; +export interface EuiSearchBoxProps extends SearchBoxConfigProps { + query: string; + onSearch: (queryText: string) => void; + isInvalid?: boolean; + title?: string; +} - static defaultProps = { +export class EuiSearchBox extends Component { + static defaultProps: Partial = { placeholder: 'Search...', incremental: false, }; - constructor(props) { - super(props); - } + private inputElement: HTMLInputElement | null = null; - componentDidUpdate(oldProps) { - if (oldProps.query !== this.props.query) { + componentDidUpdate(oldProps: EuiSearchBoxProps) { + if (oldProps.query !== this.props.query && this.inputElement != null) { this.inputElement.value = this.props.query; } } diff --git a/src/components/search_bar/search_filters.js b/src/components/search_bar/search_filters.js deleted file mode 100644 index 33a0dd250dc..00000000000 --- a/src/components/search_bar/search_filters.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { createFilter, FilterConfigType } from './filters'; -import { Query } from './query'; -import { EuiFilterGroup } from '../../components/filter_group'; - -export const SearchFiltersFiltersType = PropTypes.arrayOf(FilterConfigType); - -export class EuiSearchFilters extends Component { - static propTypes = { - query: PropTypes.instanceOf(Query).isRequired, - onChange: PropTypes.func.isRequired, - filters: SearchFiltersFiltersType, - }; - - static defaultProps = { - filters: [], - }; - - constructor(props) { - super(props); - } - - render() { - const { filters = [], query, onChange } = this.props; - const items = filters.reduce((controls, filterConfig, index) => { - if (filterConfig.available && !filterConfig.available()) { - return controls; - } - const key = `filter_${index}`; - const control = createFilter(index, filterConfig, query, onChange); - controls.push({control}); - return controls; - }, []); - return {items}; - } -} diff --git a/src/components/search_bar/search_filters.test.js b/src/components/search_bar/search_filters.test.tsx similarity index 65% rename from src/components/search_bar/search_filters.test.js rename to src/components/search_bar/search_filters.test.tsx index 9abb494787b..6d8306d6b67 100644 --- a/src/components/search_bar/search_filters.test.js +++ b/src/components/search_bar/search_filters.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { requiredProps } from '../../test'; import { shallow } from 'enzyme'; -import { EuiSearchFilters } from './search_filters'; +import { EuiSearchFilters, SearchFiltersFiltersType } from './search_filters'; import { Query } from './query'; describe('EuiSearchFilters', () => { @@ -19,23 +19,25 @@ describe('EuiSearchFilters', () => { }); test('render - with filters', () => { + const filters: SearchFiltersFiltersType = [ + { + type: 'is', + field: 'open', + name: 'Open', + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + options: () => Promise.resolve([]), + }, + ]; + const props = { ...requiredProps, onChange: () => {}, query: Query.parse(''), - filters: [ - { - type: 'is', - field: 'open', - name: 'Open', - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: () => {}, - }, - ], + filters, }; const component = shallow(); diff --git a/src/components/search_bar/search_filters.tsx b/src/components/search_bar/search_filters.tsx new file mode 100644 index 00000000000..71478b12df9 --- /dev/null +++ b/src/components/search_bar/search_filters.tsx @@ -0,0 +1,35 @@ +import React, { Component, Fragment, ReactElement } from 'react'; +import { createFilter, FilterConfig } from './filters'; +import { Query } from './query'; +import { EuiFilterGroup } from '../filter_group'; + +export type SearchFiltersFiltersType = FilterConfig[]; + +interface EuiSearchFiltersProps { + query: Query; + onChange: (query: Query) => void; + filters: SearchFiltersFiltersType; +} + +export class EuiSearchFilters extends Component { + static defaultProps: Partial = { + filters: [], + }; + + render() { + const { filters = [], query, onChange } = this.props; + + const items: ReactElement[] = []; + + filters.forEach((filterConfig, index) => { + if (filterConfig.available && !filterConfig.available()) { + return; + } + const key = `filter_${index}`; + const control = createFilter(index, filterConfig, query, onChange); + items.push({control}); + }); + + return {items}; + } +} diff --git a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap index beb5e983479..f6fd45df543 100644 --- a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap +++ b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap @@ -60,10 +60,10 @@ exports[`EuiTableFooterCell is rendered 1`] = ` `; -exports[`EuiTableFooterCell width and style accepts style attribute 1`] = ` +exports[`EuiTableFooterCell width and style Overlapping attributes resolves style and width attribute 1`] = `
`; -exports[`EuiTableFooterCell width and style accepts width attribute 1`] = ` +exports[`EuiTableFooterCell width and style accepts style attribute 1`] = `
`; -exports[`EuiTableFooterCell width and style accepts width attribute as number 1`] = ` +exports[`EuiTableFooterCell width and style accepts width attribute 1`] = `
`; -exports[`EuiTableFooterCell width and style resolves style and width attribute 1`] = ` +exports[`EuiTableFooterCell width and style accepts width attribute as number 1`] = `
`; -exports[`width and style accepts style attribute 1`] = ` +exports[`width and style Overlapping attributes resolves style and width attribute 1`] = `
`; -exports[`width and style accepts width attribute 1`] = ` +exports[`width and style accepts style attribute 1`] = `
`; -exports[`width and style accepts width attribute as number 1`] = ` +exports[`width and style accepts width attribute 1`] = `
`; -exports[`width and style resolves style and width attribute 1`] = ` +exports[`width and style accepts width attribute as number 1`] = `
`; -exports[`width and style accepts style attribute 1`] = ` +exports[`width and style Overlapping attributes resolves style and width attribute 1`] = `
`; -exports[`width and style accepts width attribute 1`] = ` +exports[`width and style accepts style attribute 1`] = `
`; -exports[`width and style accepts width attribute as number 1`] = ` +exports[`width and style accepts width attribute 1`] = `
`; -exports[`width and style resolves style and width attribute 1`] = ` +exports[`width and style accepts width attribute as number 1`] = `
{ expect(render(component)).toMatchSnapshot(); }); - test('resolves style and width attribute', () => { - const component = ( - - Test - - ); + describe('Overlapping attributes', () => { + let consoleWarn: Console['warn']; - expect(render(component)).toMatchSnapshot(); + beforeEach(() => { + consoleWarn = console.warn; + console.warn = jest.fn(); + }); + + afterEach(() => { + console.warn = consoleWarn; + }); + + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); + + expect(render(component)).toMatchSnapshot(); + + expect(console.warn).toBeCalledWith( + 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' + ); + }); }); }); }); diff --git a/src/components/table/table_header_cell.test.tsx b/src/components/table/table_header_cell.test.tsx index e5431519c35..9e72afee8da 100644 --- a/src/components/table/table_header_cell.test.tsx +++ b/src/components/table/table_header_cell.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; +import { requiredProps } from '../../test'; import { EuiTableHeaderCell } from './table_header_cell'; @@ -55,13 +55,30 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - test('resolves style and width attribute', () => { - const component = ( - - Test - - ); + describe('Overlapping attributes', () => { + let consoleWarn: Console['warn']; - expect(render(component)).toMatchSnapshot(); + beforeEach(() => { + consoleWarn = console.warn; + console.warn = jest.fn(); + }); + + afterEach(() => { + console.warn = consoleWarn; + }); + + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); + + expect(render(component)).toMatchSnapshot(); + + expect(console.warn).toBeCalledWith( + 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' + ); + }); }); }); diff --git a/src/components/table/table_row_cell.test.tsx b/src/components/table/table_row_cell.test.tsx index bb62517e77e..cc86aa4f63b 100644 --- a/src/components/table/table_row_cell.test.tsx +++ b/src/components/table/table_row_cell.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; +import { requiredProps } from '../../test'; import { EuiTableRowCell } from './table_row_cell'; -import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '../../services/alignment'; +import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '../../services'; test('renders EuiTableRowCell', () => { const component = ( @@ -95,13 +95,30 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - test('resolves style and width attribute', () => { - const component = ( - - Test - - ); + describe('Overlapping attributes', () => { + let consoleWarn: Console['warn']; - expect(render(component)).toMatchSnapshot(); + beforeEach(() => { + consoleWarn = console.warn; + console.warn = jest.fn(); + }); + + afterEach(() => { + console.warn = consoleWarn; + }); + + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); + + expect(render(component)).toMatchSnapshot(); + + expect(console.warn).toBeCalledWith( + 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' + ); + }); }); }); diff --git a/src/services/predicate/common_predicates.ts b/src/services/predicate/common_predicates.ts index 129e14348db..7142b1ef60a 100644 --- a/src/services/predicate/common_predicates.ts +++ b/src/services/predicate/common_predicates.ts @@ -24,6 +24,6 @@ export const isDate = (value: any): value is Date => { return moment.isDate(value); }; -export const isDateLike = (value: any) => { +export const isDateLike = (value: any): value is moment.Moment | Date => { return isMoment(value) || isDate(value); }; From 0bab4d4705777dde5539dd0d970aa9c3b7b91141 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 24 Feb 2020 15:38:27 +0000 Subject: [PATCH 02/10] Tweaks --- .../filters/field_value_selection_filter.tsx | 14 ++- .../filters/field_value_toggle_filter.tsx | 6 +- src/components/search_bar/filters/filters.tsx | 10 +- .../search_bar/filters/is_filter.tsx | 3 +- src/components/search_bar/index.ts | 3 - src/components/search_bar/query/ast.ts | 2 +- .../search_bar/query/ast_to_es_query_dsl.ts | 104 +++++++++--------- .../search_bar/query/date_format.ts | 1 + src/components/search_bar/query/date_value.ts | 1 + .../search_bar/query/default_syntax.test.ts | 38 +++---- .../search_bar/query/default_syntax.ts | 9 +- .../search_bar/query/execute_ast.ts | 38 +++++-- .../search_bar/query/operators.test.ts | 6 +- src/components/search_bar/search_bar.tsx | 13 ++- src/components/search_bar/search_box.tsx | 1 + .../table_footer_cell.test.tsx.snap | 16 +-- .../table_header_cell.test.tsx.snap | 16 +-- .../table_row_cell.test.tsx.snap | 16 +-- .../table/table_footer_cell.test.tsx | 4 +- .../table/table_header_cell.test.tsx | 4 +- src/components/table/table_row_cell.test.tsx | 4 +- 21 files changed, 170 insertions(+), 139 deletions(-) diff --git a/src/components/search_bar/filters/field_value_selection_filter.tsx b/src/components/search_bar/filters/field_value_selection_filter.tsx index e9a35aed9da..70d4c8c46b5 100644 --- a/src/components/search_bar/filters/field_value_selection_filter.tsx +++ b/src/components/search_bar/filters/field_value_selection_filter.tsx @@ -8,10 +8,11 @@ import { EuiLoadingChart } from '../../loading'; import { EuiSpacer } from '../../spacer'; import { EuiIcon } from '../../icon'; import { Query } from '../query'; +import { Clause, Value } from '../query/ast'; interface FieldValueOptionType { field?: string; - value: any; + value: Value; name?: string; view?: ReactNode; } @@ -44,7 +45,7 @@ export interface FieldValueSelectionFilterProps { index: number; config: FieldValueSelectionFilterConfigType; query: Query; - onChange: (value: any) => void; + onChange: (query: Query) => void; autoClose?: boolean; } @@ -205,7 +206,11 @@ export class FieldValueSelectionFilter extends Component< return option.name || option.value.toString(); } - onOptionClick(field: string, value: any, checked: 'on' | 'off' | undefined) { + onOptionClick( + field: string, + value: Value, + checked: 'on' | 'off' | undefined + ) { const multiSelect = this.resolveMultiSelect(); const { autoClose } = this.props; @@ -421,8 +426,7 @@ export class FieldValueSelectionFilter extends Component< ); } - // FIXME - resolveChecked(clause: any): 'on' | 'off' | undefined { + resolveChecked(clause: Clause | undefined): 'on' | 'off' | undefined { if (clause) { return Query.isMust(clause) ? 'on' : 'off'; } diff --git a/src/components/search_bar/filters/field_value_toggle_filter.tsx b/src/components/search_bar/filters/field_value_toggle_filter.tsx index 058f7581cbb..772c1a32361 100644 --- a/src/components/search_bar/filters/field_value_toggle_filter.tsx +++ b/src/components/search_bar/filters/field_value_toggle_filter.tsx @@ -2,11 +2,12 @@ import React, { Component } from 'react'; import { EuiFilterButton } from '../../filter_group'; import { isNil } from '../../../services/predicate'; import { Query } from '../query'; +import { Clause, Value } from '../query/ast'; export interface FieldValueToggleFilterConfigType { type: 'field_value_toggle'; field: string; - value: string | number | boolean; + value: Value; name: string; negatedName?: string; available?: () => boolean; @@ -23,8 +24,7 @@ export interface FieldValueToggleFilterProps { export class FieldValueToggleFilter extends Component< FieldValueToggleFilterProps > { - // FIXME - resolveDisplay(clause: any) { + resolveDisplay(clause: Clause | undefined) { const { name, negatedName } = this.props.config; if (isNil(clause)) { return { hasActiveFilters: false, name }; diff --git a/src/components/search_bar/filters/filters.tsx b/src/components/search_bar/filters/filters.tsx index e473d088455..c3e67f3b18d 100644 --- a/src/components/search_bar/filters/filters.tsx +++ b/src/components/search_bar/filters/filters.tsx @@ -22,9 +22,11 @@ export const createFilter = ( ) => { const props = { index, query, onChange }; - // We don't put config in `props` above because TS will give it a wider - // type that we want. Once we've checked `config.type` below, its type - // is narrowed correctly. + // We don't put `config` into `props` above because until we check + // `config.type`, TS only knows that it's a `FilterConfig`, and that type + // is used to define `props` as well. Once we've checked `config.type` + // below, its type is narrowed correctly, hence we pass down `config` + // separately. switch (config.type) { case 'is': return ; @@ -39,7 +41,7 @@ export const createFilter = ( return ; default: - // @ts-ignore TS knows that we can't get here + // @ts-ignore TS knows that we've checked `config.type` exhaustively throw new Error(`Unknown search filter type [${config.type}]`); } }; diff --git a/src/components/search_bar/filters/is_filter.tsx b/src/components/search_bar/filters/is_filter.tsx index 9b0169841d4..2d51139f4ef 100644 --- a/src/components/search_bar/filters/is_filter.tsx +++ b/src/components/search_bar/filters/is_filter.tsx @@ -33,8 +33,7 @@ export class IsFilter extends Component { }; } - // FIXME - valueChanged(field: any, checked: boolean) { + valueChanged(field: string, checked: boolean) { const query = checked ? this.props.query.removeIsClause(field) : this.props.query.addMustIsClause(field); diff --git a/src/components/search_bar/index.ts b/src/components/search_bar/index.ts index 570c9f1e40d..919d9f5333e 100644 --- a/src/components/search_bar/index.ts +++ b/src/components/search_bar/index.ts @@ -1,6 +1,3 @@ export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; export { SearchBoxConfigProps } from './search_box'; export { SearchFiltersFiltersType } from './search_filters'; - -// TODO: Some related types are defined in basic_table/in_memory_table. -// Use and remove them when TypeScriptification is done. diff --git a/src/components/search_bar/query/ast.ts b/src/components/search_bar/query/ast.ts index 77837f2eda2..09af3e94d73 100644 --- a/src/components/search_bar/query/ast.ts +++ b/src/components/search_bar/query/ast.ts @@ -353,7 +353,7 @@ export class _AST { return false; } - // We can apply this type cast due to the filter above + // We can apply this type cast due to the `isArray` filter above return isNil(value) || arrayIncludesValue(clause.value as Value[], value); } diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.ts b/src/components/search_bar/query/ast_to_es_query_dsl.ts index 8689d7800b2..209d9dc737f 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.ts +++ b/src/components/search_bar/query/ast_to_es_query_dsl.ts @@ -11,15 +11,55 @@ import { } from './ast'; import { isArray, isDateLike, isString } from '../../../services/predicate'; -interface Options { - defaultFields?: string[]; - extraMustQueries?: any[]; // FIXME - extraMustNotQueries?: any[]; // FIXME - termValuesToQuery?: (terms: Value[], options: {}) => any; - fieldValuesToQuery?: (terms: string, options: {}) => any; - isFlagToQuery?: (flag: string, on: boolean) => any; +export interface QueryContainer { + bool?: BoolQuery; + match_all?: {}; + match?: object; + match_phrase?: object; + range?: object; + term?: object; + simple_query_string?: object; +} + +interface TermsQuery { + must: Value[]; + mustNot: Value[]; +} + +interface BoolQuery { + must?: QueryContainer[]; + must_not?: QueryContainer[]; + should?: QueryContainer[]; } +interface FieldsQuery { + must: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; + mustNot: { + and: { + [field: string]: any; + }; + or: { + [field: string]: any; + }; + }; +} + +type Options = Partial<{ + defaultFields: string[]; + extraMustQueries: QueryContainer[]; + extraMustNotQueries: QueryContainer[]; + termValuesToQuery: (terms: Value[], options: {}) => QueryContainer; + fieldValuesToQuery: (terms: string, options: {}) => QueryContainer; + isFlagToQuery: (flag: string, on: boolean) => QueryContainer; +}>; + const processDateOperation = (value: DateValue, operator?: OperatorType) => { const { granularity, resolve } = value; let expression = printIso8601(resolve()); @@ -167,15 +207,17 @@ export const _isFlagToQuery = (flag: string, on: boolean) => { }; const collectTerms = (clauses: TermClause[]) => { - const initialVar: TermsQuery = { must: [], mustNot: [] }; - return clauses.reduce((values, clause) => { + const values: TermsQuery = { must: [], mustNot: [] }; + + for (const clause of clauses) { if (AST.Match.isMustClause(clause)) { values.must.push(clause.value); } else { values.mustNot.push(clause.value); } - return values; - }, initialVar); + } + + return values; }; const collectFields = (clauses: FieldClause[]) => { @@ -252,7 +294,7 @@ const clausesToEsQueryDsl = ( must.push(isFlagToQuery(clause.flag, AST.Match.isMustClause(clause))); }); - const mustNot = []; + const mustNot: QueryContainer[] = []; mustNot.push(...extraMustNotQueries); const termMustNotQuery = termValuesToQuery(terms.mustNot, options); if (termMustNotQuery) { @@ -276,44 +318,6 @@ const clausesToEsQueryDsl = ( return bool; }; -interface TermsQuery { - must: Value[]; - mustNot: Value[]; -} - -export interface QueryContainer { - bool?: BoolQuery; - match_all?: {}; - match?: object; - match_phrase?: object; - range?: object; -} - -interface BoolQuery { - must?: QueryContainer[]; - must_not?: QueryContainer[]; - should?: QueryContainer[]; -} - -interface FieldsQuery { - must: { - and: { - [field: string]: any; - }; - or: { - [field: string]: any; - }; - }; - mustNot: { - and: { - [field: string]: any; - }; - or: { - [field: string]: any; - }; - }; -} - const EMPTY_TERMS: TermsQuery = { must: [], mustNot: [] }; const EMPTY_FIELDS: FieldsQuery = { must: { and: {}, or: {} }, diff --git a/src/components/search_bar/query/date_format.ts b/src/components/search_bar/query/date_format.ts index 9a1ef96f7d2..23435fe0b86 100644 --- a/src/components/search_bar/query/date_format.ts +++ b/src/components/search_bar/query/date_format.ts @@ -1,4 +1,5 @@ import { dateFormatAliases } from '../../../services/format'; +// ESLint doesn't realise that we can import Moment directly. // eslint-disable-next-line import/named import moment, { Moment, MomentInput } from 'moment'; diff --git a/src/components/search_bar/query/date_value.ts b/src/components/search_bar/query/date_value.ts index 694f0eced74..95c063d6a1e 100644 --- a/src/components/search_bar/query/date_value.ts +++ b/src/components/search_bar/query/date_value.ts @@ -4,6 +4,7 @@ import { dateGranularity, GranularityType, } from './date_format'; +// ESLint doesn't realise that we can import Moment directly. // eslint-disable-next-line import/named import moment, { MomentInput } from 'moment'; diff --git a/src/components/search_bar/query/default_syntax.test.ts b/src/components/search_bar/query/default_syntax.test.ts index 83ace7d72dc..5ec75d0b0e7 100644 --- a/src/components/search_bar/query/default_syntax.test.ts +++ b/src/components/search_bar/query/default_syntax.test.ts @@ -34,33 +34,33 @@ describe('defaultSyntax', () => { expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('term-1'); - clause = ast.getTermClause('term-2')!!; + clause = ast.getTermClause('term-2')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('term-2'); - clause = ast.getTermClause('-term-3')!!; + clause = ast.getTermClause('-term-3')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('-term-3'); - clause = ast.getSimpleFieldClause('name-1', 'dash-1')!!; + clause = ast.getSimpleFieldClause('name-1', 'dash-1')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('name-1'); expect(clause.value).toBe('dash-1'); - clause = ast.getSimpleFieldClause('name-2', 'dash-2')!!; + clause = ast.getSimpleFieldClause('name-2', 'dash-2')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name-2'); expect(clause.value).toBe('dash-2'); - clause = ast.getSimpleFieldClause('-name-3', 'dash-3')!!; + clause = ast.getSimpleFieldClause('-name-3', 'dash-3')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -83,7 +83,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('name'); expect(clause.value).toBe('👸Queen_Elizabeth'); - clause = ast.getTermClause('🤴King_Henry')!!; + clause = ast.getTermClause('🤴King_Henry')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -104,19 +104,19 @@ describe('defaultSyntax', () => { expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe(':'); - clause = ast.getTermClause('\\')!!; + clause = ast.getTermClause('\\')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('\\'); - clause = ast.getTermClause('(')!!; + clause = ast.getTermClause('(')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('('); - clause = ast.getTermClause(')')!!; + clause = ast.getTermClause(')')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -214,7 +214,7 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6)!!; + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -239,14 +239,14 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6)!!; + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5)!!; + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -271,14 +271,14 @@ describe('defaultSyntax', () => { expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getSimpleFieldClause('age', 6)!!; + clause = ast.getSimpleFieldClause('age', 6)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.field).toBe('age'); expect(clause.value).toBe(6); - clause = ast.getSimpleFieldClause('age', 5)!!; + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -302,7 +302,7 @@ describe('defaultSyntax', () => { expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar')!!; + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); @@ -325,7 +325,7 @@ describe('defaultSyntax', () => { expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getTermClause('bar')!!; + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); @@ -348,20 +348,20 @@ describe('defaultSyntax', () => { expect(AST.Match.isMustClause(clause)).toBe(true); expect(clause.value).toBe('foo'); - clause = ast.getSimpleFieldClause('name', 'john')!!; + clause = ast.getSimpleFieldClause('name', 'john')!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.field).toBe('name'); expect(clause.value).toBe('john'); - clause = ast.getTermClause('bar')!!; + clause = ast.getTermClause('bar')!; expect(clause).toBeDefined(); expect(AST.Term.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(false); expect(clause.value).toBe('bar'); - clause = ast.getSimpleFieldClause('age', 5)!!; + clause = ast.getSimpleFieldClause('age', 5)!; expect(clause).toBeDefined(); expect(AST.Field.isInstance(clause)).toBe(true); expect(AST.Match.isMustClause(clause)).toBe(true); diff --git a/src/components/search_bar/query/default_syntax.ts b/src/components/search_bar/query/default_syntax.ts index f94634bc860..8d7b19b2715 100644 --- a/src/components/search_bar/query/default_syntax.ts +++ b/src/components/search_bar/query/default_syntax.ts @@ -2,7 +2,7 @@ import { _AST, AST, Clause, OperatorType, Value } from './ast'; import { isArray, isString, isDateLike } from '../../../services/predicate'; import { dateFormat as defaultDateFormat } from './date_format'; import { DateValue, dateValueParser, isDateValue } from './date_value'; -// @ts-ignore +// @ts-ignore This is a Babel plugin that parses inline PEG grammars. import peg from 'pegjs-inline-precompile'; // eslint-disable-line import/no-unresolved const parser = peg` @@ -331,7 +331,8 @@ const resolveFieldValue = ( ): Value | Value[] => { const { schema, error, parseDate } = ctx; if (isArray(valueExpression)) { - // FIXME: I don't know if this cast is valid + // I don't know if this cast is valid. This function is called recursively and + // doesn't apply any kind of flat-map. return valueExpression.map( exp => resolveFieldValue(field, exp, ctx) as Value ); @@ -396,6 +397,8 @@ const resolveFieldValue = ( return number; case 'boolean': + // FIXME This would also match 'lion'. It should really anchor the match + // and the start and end of the input. const boolean = !!expression.match(/true|yes|on/i); validateFieldValue( field, @@ -420,7 +423,7 @@ const resolveFieldValue = ( } }; -const printValue = (value: any, options: Options) => { +const printValue = (value: Value, options: Options) => { if (isDateValue(value)) { return `'${value.text}'`; } diff --git a/src/components/search_bar/query/execute_ast.ts b/src/components/search_bar/query/execute_ast.ts index e2a82cdc824..e6cb1490008 100644 --- a/src/components/search_bar/query/execute_ast.ts +++ b/src/components/search_bar/query/execute_ast.ts @@ -39,11 +39,11 @@ interface Explain { value?: Value | Value[]; flag?: string; match?: MatchType; - operator?: any; // FIXME can't be bothered typing this right now + operator?: any; // It's not really worth specifying this at the moment } -const defaultIsClauseMatcher = ( - item: any, +const defaultIsClauseMatcher = ( + item: T, clause: IsClause, explain?: Explain[] ) => { @@ -57,8 +57,8 @@ const defaultIsClauseMatcher = ( return hit; }; -const fieldClauseMatcher = ( - item: any, +const fieldClauseMatcher = ( + item: T, field: string, clauses: FieldClause[] = [], explain?: Explain[] @@ -85,6 +85,8 @@ const fieldClauseMatcher = ( }); }; +// You might think that we could specify `item: T` here and do something +// with `keyof`, but that wouldn't work with `nested.field.name` const extractStringFieldsFromItem = (item: any) => { return Object.keys(item).reduce( (fields, key) => { @@ -97,9 +99,9 @@ const extractStringFieldsFromItem = (item: any) => { ); }; -const termClauseMatcher = ( - item: any, - fields: string[], +const termClauseMatcher = ( + item: T, + fields: string[] | undefined, clauses: TermClause[] = [], explain?: Explain[] ) => { @@ -136,18 +138,20 @@ const termClauseMatcher = ( }); }; -export const createFilter = ( +export const createFilter = ( ast: _AST, - defaultFields: string[], + defaultFields: string[] | undefined, isClauseMatcher = defaultIsClauseMatcher, explain = false ) => { // Return items which pass ALL conditions: matches the terms entered, the specified field values, // and the specified "is" clauses. - return (item: any) => { + return (item: T) => { const explainLines = explain ? ([] as Explain[]) : undefined; if (explainLines) { + // @ts-ignore technically, we could require T to extend `{ __explain?: Explain[] }` but that seems + // like a ridiculous requirement on the caller. item[EXPLAIN_FIELD] = explainLines; } @@ -200,7 +204,17 @@ export const createFilter = ( }; }; -export function executeAst(ast: _AST, items: T[], options: any = {}): T[] { +interface Options { + isClauseMatcher?: typeof defaultIsClauseMatcher; + defaultFields?: string[]; + explain?: boolean; +} + +export function executeAst( + ast: _AST, + items: T[], + options: Options = {} +): T[] { const { isClauseMatcher, defaultFields, explain } = options; const filter = createFilter(ast, defaultFields, isClauseMatcher, explain); return items.filter(filter); diff --git a/src/components/search_bar/query/operators.test.ts b/src/components/search_bar/query/operators.test.ts index 64f5426ad32..a86a9ac586d 100644 --- a/src/components/search_bar/query/operators.test.ts +++ b/src/components/search_bar/query/operators.test.ts @@ -6,10 +6,12 @@ import { Granularity } from './date_format'; const random = new Random(); +type TimeUnits = 'hours' | 'days' | 'weeks' | 'months' | 'years'; + const laterMoment = ( date: moment.MomentInput, count: number, - units: 'hours' | 'days' | 'weeks' | 'months' | 'years' + units: TimeUnits ) => { const later = moment(date); later.add(count, units); @@ -19,7 +21,7 @@ const laterMoment = ( const earlierMoment = ( date: moment.MomentInput, count: number, - units: 'hours' | 'days' | 'weeks' | 'months' | 'years' + units: TimeUnits ) => { const later = moment(date); later.subtract(count, units); diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index ba2370e63c9..026f6753247 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -1,7 +1,7 @@ import React, { Component, ReactElement } from 'react'; import { isString } from '../../services/predicate'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; -import { EuiSearchBox, SearchBoxConfigProps } from './search_box'; +import { EuiSearchBox, SchemaType, SearchBoxConfigProps } from './search_box'; import { EuiSearchFilters, SearchFiltersFiltersType } from './search_filters'; import { Query } from './query'; import { CommonProps } from '../common'; @@ -61,7 +61,10 @@ const parseQuery = ( query: QueryType | undefined, props: EuiSearchBarProps ): Query => { - const schema = props.box ? props.box.schema : undefined; + let schema: SchemaType | undefined = undefined; + if (props.box && props.box.schema && typeof props.box.schema === 'object') { + schema = props.box.schema; + } const dateFormat = props.dateFormat; const parseOptions = { schema, dateFormat }; if (!query) { @@ -76,6 +79,8 @@ interface State { error: null | Error; } +// `state.query` is never null, but can be passed as `null` to `notifyControllingParent` +// when `error` is not null. type StateWithOptionalQuery = Omit & { query: Query | null }; export class EuiSearchBar extends Component { @@ -160,9 +165,7 @@ export class EuiSearchBar extends Component { if (Array.isArray(tools)) { return tools.map(tool => ( - // There's a mismatch somewhere around how the key attribute / - // property is defined, such that `null` is not allowed - + {tool} )); diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 4805331f027..d48ebaea74a 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -13,6 +13,7 @@ export interface SearchBoxConfigProps extends CommonProps { incremental?: boolean; // Boolean values are not meaningful to this component, but are allowed so that other // components can use e.g. a true value to mean "auto-derive a schema". See EuiInMemoryTable. + // Admittedly, this is a bit of a hack. schema?: SchemaType | boolean; } diff --git a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap index f6fd45df543..7f5c72bd322 100644 --- a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap +++ b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap @@ -60,10 +60,10 @@ exports[`EuiTableFooterCell is rendered 1`] = ` `; -exports[`EuiTableFooterCell width and style Overlapping attributes resolves style and width attribute 1`] = ` +exports[`EuiTableFooterCell width and style accepts style attribute 1`] = `
`; -exports[`EuiTableFooterCell width and style accepts style attribute 1`] = ` +exports[`EuiTableFooterCell width and style accepts width attribute 1`] = `
`; -exports[`EuiTableFooterCell width and style accepts width attribute 1`] = ` +exports[`EuiTableFooterCell width and style accepts width attribute as number 1`] = `
`; -exports[`EuiTableFooterCell width and style accepts width attribute as number 1`] = ` +exports[`EuiTableFooterCell width and style resolves overlapping attributes in style vs width 1`] = `
`; -exports[`width and style Overlapping attributes resolves style and width attribute 1`] = ` +exports[`width and style accepts style attribute 1`] = `
`; -exports[`width and style accepts style attribute 1`] = ` +exports[`width and style accepts width attribute 1`] = `
`; -exports[`width and style accepts width attribute 1`] = ` +exports[`width and style accepts width attribute as number 1`] = `
`; -exports[`width and style accepts width attribute as number 1`] = ` +exports[`width and style resolves overlapping attributes in style vs width 1`] = `
`; -exports[`width and style Overlapping attributes resolves style and width attribute 1`] = ` +exports[`width and style accepts style attribute 1`] = `
`; -exports[`width and style accepts style attribute 1`] = ` +exports[`width and style accepts width attribute 1`] = `
`; -exports[`width and style accepts width attribute 1`] = ` +exports[`width and style accepts width attribute as number 1`] = `
`; -exports[`width and style accepts width attribute as number 1`] = ` +exports[`width and style resolves overlapping attributes in style vs width 1`] = `
{ expect(render(component)).toMatchSnapshot(); }); - describe('Overlapping attributes', () => { + describe('resolves overlapping attributes', () => { let consoleWarn: Console['warn']; beforeEach(() => { @@ -72,7 +72,7 @@ describe('EuiTableFooterCell', () => { console.warn = consoleWarn; }); - test('resolves style and width attribute', () => { + test('in style vs width', () => { const component = ( Test diff --git a/src/components/table/table_header_cell.test.tsx b/src/components/table/table_header_cell.test.tsx index 9e72afee8da..f9b4c7bfa2f 100644 --- a/src/components/table/table_header_cell.test.tsx +++ b/src/components/table/table_header_cell.test.tsx @@ -55,7 +55,7 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - describe('Overlapping attributes', () => { + describe('resolves overlapping attributes', () => { let consoleWarn: Console['warn']; beforeEach(() => { @@ -67,7 +67,7 @@ describe('width and style', () => { console.warn = consoleWarn; }); - test('resolves style and width attribute', () => { + test('in style vs width', () => { const component = ( Test diff --git a/src/components/table/table_row_cell.test.tsx b/src/components/table/table_row_cell.test.tsx index cc86aa4f63b..9200f7cb1b0 100644 --- a/src/components/table/table_row_cell.test.tsx +++ b/src/components/table/table_row_cell.test.tsx @@ -95,7 +95,7 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - describe('Overlapping attributes', () => { + describe('resolves overlapping attributes', () => { let consoleWarn: Console['warn']; beforeEach(() => { @@ -107,7 +107,7 @@ describe('width and style', () => { console.warn = consoleWarn; }); - test('resolves style and width attribute', () => { + test('in style vs width', () => { const component = ( Test From 5c22b4561f96102abea1683f5e6b343e20f78f69 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 24 Feb 2020 16:12:43 +0000 Subject: [PATCH 03/10] Revert table test fixes --- .../table_footer_cell.test.tsx.snap | 2 +- .../table_header_cell.test.tsx.snap | 2 +- .../table_row_cell.test.tsx.snap | 2 +- .../table/table_footer_cell.test.tsx | 33 +++++------------ .../table/table_header_cell.test.tsx | 33 +++++------------ src/components/table/table_row_cell.test.tsx | 35 +++++-------------- 6 files changed, 28 insertions(+), 79 deletions(-) diff --git a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap index 7f5c72bd322..beb5e983479 100644 --- a/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap +++ b/src/components/table/__snapshots__/table_footer_cell.test.tsx.snap @@ -111,7 +111,7 @@ exports[`EuiTableFooterCell width and style accepts width attribute as number 1` `; -exports[`EuiTableFooterCell width and style resolves overlapping attributes in style vs width 1`] = ` +exports[`EuiTableFooterCell width and style resolves style and width attribute 1`] = ` `; -exports[`width and style resolves overlapping attributes in style vs width 1`] = ` +exports[`width and style resolves style and width attribute 1`] = ` `; -exports[`width and style resolves overlapping attributes in style vs width 1`] = ` +exports[`width and style resolves style and width attribute 1`] = ` { expect(render(component)).toMatchSnapshot(); }); - describe('resolves overlapping attributes', () => { - let consoleWarn: Console['warn']; - - beforeEach(() => { - consoleWarn = console.warn; - console.warn = jest.fn(); - }); - - afterEach(() => { - console.warn = consoleWarn; - }); - - test('in style vs width', () => { - const component = ( - - Test - - ); - - expect(render(component)).toMatchSnapshot(); + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); - expect(console.warn).toBeCalledWith( - 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' - ); - }); + expect(render(component)).toMatchSnapshot(); }); }); }); diff --git a/src/components/table/table_header_cell.test.tsx b/src/components/table/table_header_cell.test.tsx index f9b4c7bfa2f..e5431519c35 100644 --- a/src/components/table/table_header_cell.test.tsx +++ b/src/components/table/table_header_cell.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from 'enzyme'; -import { requiredProps } from '../../test'; +import { requiredProps } from '../../test/required_props'; import { EuiTableHeaderCell } from './table_header_cell'; @@ -55,30 +55,13 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - describe('resolves overlapping attributes', () => { - let consoleWarn: Console['warn']; - - beforeEach(() => { - consoleWarn = console.warn; - console.warn = jest.fn(); - }); - - afterEach(() => { - console.warn = consoleWarn; - }); - - test('in style vs width', () => { - const component = ( - - Test - - ); - - expect(render(component)).toMatchSnapshot(); + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); - expect(console.warn).toBeCalledWith( - 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' - ); - }); + expect(render(component)).toMatchSnapshot(); }); }); diff --git a/src/components/table/table_row_cell.test.tsx b/src/components/table/table_row_cell.test.tsx index 9200f7cb1b0..bb62517e77e 100644 --- a/src/components/table/table_row_cell.test.tsx +++ b/src/components/table/table_row_cell.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { render } from 'enzyme'; -import { requiredProps } from '../../test'; +import { requiredProps } from '../../test/required_props'; import { EuiTableRowCell } from './table_row_cell'; -import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '../../services'; +import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '../../services/alignment'; test('renders EuiTableRowCell', () => { const component = ( @@ -95,30 +95,13 @@ describe('width and style', () => { expect(render(component)).toMatchSnapshot(); }); - describe('resolves overlapping attributes', () => { - let consoleWarn: Console['warn']; - - beforeEach(() => { - consoleWarn = console.warn; - console.warn = jest.fn(); - }); - - afterEach(() => { - console.warn = consoleWarn; - }); - - test('in style vs width', () => { - const component = ( - - Test - - ); - - expect(render(component)).toMatchSnapshot(); + test('resolves style and width attribute', () => { + const component = ( + + Test + + ); - expect(console.warn).toBeCalledWith( - 'Two `width` properties were provided. Provide only one of `style.width` or `width` to avoid conflicts.' - ); - }); + expect(render(component)).toMatchSnapshot(); }); }); From f0a0bba8d3cfac8df58244889b3ce02b82daf412 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 24 Feb 2020 17:04:34 +0000 Subject: [PATCH 04/10] Docs fixes --- .../src/views/search_bar/controlled_search_bar.js | 3 +++ src-docs/src/views/search_bar/search_bar.js | 3 +++ src-docs/src/views/search_bar/search_bar_example.js | 12 ++++++------ src-docs/src/views/search_bar/search_bar_filters.js | 3 +++ .../views/tables/in_memory/in_memory_selection.js | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src-docs/src/views/search_bar/controlled_search_bar.js b/src-docs/src/views/search_bar/controlled_search_bar.js index 5cb461c49dc..03ffd47ae3a 100644 --- a/src-docs/src/views/search_bar/controlled_search_bar.js +++ b/src-docs/src/views/search_bar/controlled_search_bar.js @@ -154,6 +154,9 @@ export class ControlledSearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js index 2f9dd3334b3..28c6ecfac9b 100644 --- a/src-docs/src/views/search_bar/search_bar.js +++ b/src-docs/src/views/search_bar/search_bar.js @@ -136,6 +136,9 @@ export class SearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar_example.js b/src-docs/src/views/search_bar/search_bar_example.js index 31b59653e3e..27f701293d0 100644 --- a/src-docs/src/views/search_bar/search_bar_example.js +++ b/src-docs/src/views/search_bar/search_bar_example.js @@ -37,9 +37,9 @@ export const SearchBarExample = { text: (

- A EuiSearchBar is a toolbar that enables the user - to create/define a search query. This can be done either by entering - the query syntax in a search box or by clicking any of the + An EuiSearchBar is a toolbar that enables the + user to create/define a search query. This can be done either by + entering the query syntax in a search box or by clicking any of the configured filters. The query language is not meant to be full blown search language for arbitrary data (e.g. as required in the Discover App in Kibana), yet it does provide some useful features: @@ -231,8 +231,8 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have its query controlled by a - parent component by passing the query prop. + An EuiSearchBar can have its query controlled by + a parent component by passing the query prop. Changes to the query will be passed back up through the{' '} onChange callback where the new query must be stored in state and passed back into the search bar. @@ -256,7 +256,7 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have custom filter dropdowns + An EuiSearchBar can have custom filter dropdowns that control how a user can search.

diff --git a/src-docs/src/views/search_bar/search_bar_filters.js b/src-docs/src/views/search_bar/search_bar_filters.js index 5a5efe6eac1..1b74a967672 100644 --- a/src-docs/src/views/search_bar/search_bar_filters.js +++ b/src-docs/src/views/search_bar/search_bar_filters.js @@ -122,6 +122,9 @@ export class SearchBarFilters extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.js b/src-docs/src/views/tables/in_memory/in_memory_selection.js index 44c6721872e..630f07f03f4 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection.js +++ b/src-docs/src/views/tables/in_memory/in_memory_selection.js @@ -97,7 +97,7 @@ export class Table extends Component { renderToolsLeft() { const selection = this.state.control_columns; - if (selection.length === 0) { + if (!selection || selection.length === 0) { return; } From cebb15fea5508c44e4f99384ccf6f9037c430e79 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Mon, 24 Feb 2020 17:10:53 +0000 Subject: [PATCH 05/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d87c4fa97f7..8bc652c6d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Converted `EuiSearchBar` to Typescript ([#2909](https://github.com/elastic/eui/pull/2909)) - Converted `EuiCodeEditor` to Typescript ([#2836](https://github.com/elastic/eui/pull/2836)) - Converted `EuiCode` and `EuiCodeBlock` and to Typescript ([#2835](https://github.com/elastic/eui/pull/2835)) - Converted `EuiFilePicker` to TypeScript ([#2832](https://github.com/elastic/eui/issues/2832)) From a8a0f28ea63ab4126efcb6091bbfcf8365a33a2a Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Sun, 1 Mar 2020 11:42:51 +0000 Subject: [PATCH 06/10] Address review feedback --- .../basic_table/in_memory_table.test.tsx | 11 +++++++++-- src/components/basic_table/in_memory_table.tsx | 14 ++++++++++---- .../search_bar/query/ast_to_es_query_dsl.ts | 5 +++-- src/components/search_bar/query/default_syntax.ts | 6 +++--- src/components/search_bar/query/operators.ts | 4 ++-- src/components/search_bar/query/query.ts | 4 ++-- src/components/search_bar/search_bar.tsx | 4 ++-- src/components/search_bar/search_box.tsx | 8 +++++--- 8 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 3aa378bef45..415d1498e59 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -649,6 +649,7 @@ describe('EuiInMemoryTable', () => { pagination: true, sorting: true, search: { + onChange: () => {}, defaultQuery: 'name:name1', box: { incremental: true, @@ -699,7 +700,9 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: {}, + search: { + onChange: () => true, + }, className: 'testTable', }; @@ -765,7 +768,10 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: { defaultQuery: 'No' }, + search: { + onChange: () => {}, + defaultQuery: 'No', + }, className: 'testTable', message: No items found!, }; @@ -807,6 +813,7 @@ describe('EuiInMemoryTable', () => { }, ], search: { + onChange: () => {}, defaultQuery: 'No', }, className: 'testTable', diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index ef9091c116f..6dca1b40ac0 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -328,14 +328,17 @@ export class EuiInMemoryTable extends Component< }; onQueryChange = ({ query, queryText, error }: onChangeArgument) => { - if (isEuiSearchBarProps(this.props.search)) { - const search = this.props.search; + const { search } = this.props; + if (isEuiSearchBarProps(search)) { if (search.onChange) { - search.onChange({ + const shouldQueryInMemory = search.onChange({ query, queryText, error, }); + if (!shouldQueryInMemory) { + return; + } } } @@ -356,7 +359,10 @@ export class EuiInMemoryTable extends Component< searchBarProps = _searchBarProps; if (searchBarProps.box && searchBarProps.box.schema === true) { - searchBarProps.box.schema = this.resolveSearchSchema(); + searchBarProps.box = { + ...searchBarProps.box, + schema: this.resolveSearchSchema(), + }; } } diff --git a/src/components/search_bar/query/ast_to_es_query_dsl.ts b/src/components/search_bar/query/ast_to_es_query_dsl.ts index 209d9dc737f..d53e6af627a 100644 --- a/src/components/search_bar/query/ast_to_es_query_dsl.ts +++ b/src/components/search_bar/query/ast_to_es_query_dsl.ts @@ -10,6 +10,7 @@ import { Value, } from './ast'; import { isArray, isDateLike, isString } from '../../../services/predicate'; +import { keysOf } from '../../common'; export interface QueryContainer { bool?: BoolQuery; @@ -111,11 +112,11 @@ export const _fieldValuesToQuery = ( ) => { const queries: QueryContainer[] = []; - (Object.keys(operations) as OperatorType[]).forEach(operator => { + keysOf(operations).forEach(operator => { const values = operations[operator]; switch (operator) { case AST.Operator.EQ: - const terms: any[] = []; + const terms: Value[] = []; const phrases: string[] = []; const dates: DateValue[] = []; diff --git a/src/components/search_bar/query/default_syntax.ts b/src/components/search_bar/query/default_syntax.ts index 8d7b19b2715..3f144e42c5b 100644 --- a/src/components/search_bar/query/default_syntax.ts +++ b/src/components/search_bar/query/default_syntax.ts @@ -245,7 +245,7 @@ interface ValueExpression { location: Location; } -export interface Options { +export interface ParseOptions { dateFormat?: any; schema?: any; escapeValue?: (value: any) => string; @@ -423,7 +423,7 @@ const resolveFieldValue = ( } }; -const printValue = (value: Value, options: Options) => { +const printValue = (value: Value, options: ParseOptions) => { if (isDateValue(value)) { return `'${value.text}'`; } @@ -462,7 +462,7 @@ const resolveOperator = (operator: OperatorType) => { }; export const defaultSyntax = Object.freeze({ - parse: (query: string, options: Options = {}) => { + parse: (query: string, options: ParseOptions = {}) => { const dateFormat = options.dateFormat || defaultDateFormat; const parseDate = dateValueParser(dateFormat); const schema = options.schema || {}; diff --git a/src/components/search_bar/query/operators.ts b/src/components/search_bar/query/operators.ts index e3680f608be..f9e2d192692 100644 --- a/src/components/search_bar/query/operators.ts +++ b/src/components/search_bar/query/operators.ts @@ -32,7 +32,7 @@ const resolveValueAsDate = (value: FieldValue) => { if (moment.isDate(value) || isNumber(value)) { return moment(value); } - return dateFormat.parse((value || '').toString()); + return dateFormat.parse(String(value)); }; type Options = Partial<{ @@ -147,7 +147,7 @@ const greaterThen = ( } if (isString(fieldValue)) { - const str = clauseValue ? clauseValue.toString() : ''; + const str = String(clauseValue); return inclusive ? fieldValue >= str : fieldValue > str; } diff --git a/src/components/search_bar/query/query.ts b/src/components/search_bar/query/query.ts index 05c8de8f432..03e81dee5b2 100644 --- a/src/components/search_bar/query/query.ts +++ b/src/components/search_bar/query/query.ts @@ -1,4 +1,4 @@ -import { defaultSyntax } from './default_syntax'; +import { defaultSyntax, ParseOptions } from './default_syntax'; import { executeAst } from './execute_ast'; import { isNil, isString } from '../../../services/predicate'; import { astToEsQueryDsl } from './ast_to_es_query_dsl'; @@ -11,7 +11,7 @@ import { _AST, AST, Clause, Operator, OperatorType, Value } from './ast'; * It is immutable - all mutating operations return a new (mutated) query instance. */ export class Query { - static parse(text: string, options?: {}, syntax = defaultSyntax) { + static parse(text: string, options?: ParseOptions, syntax = defaultSyntax) { return new Query(syntax.parse(text, options), syntax, text); } diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 026f6753247..3f7d1789ac6 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -13,11 +13,11 @@ export type QueryType = Query | string; type Tools = ReactElement | ReactElement[]; export interface EuiSearchBarProps extends CommonProps { - onChange?: (args: { + onChange: (args: { query: Query | null; queryText: string; error: Error | null; - }) => void; + }) => void | boolean; /** The initial query the bar will hold when first mounted diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index fdca03c7696..6e989dcdd16 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -3,8 +3,8 @@ import { EuiFieldSearch } from '../form'; import { CommonProps } from '../common'; export interface SchemaType { - strict: boolean; - fields: any; + strict?: boolean; + fields?: any; flags?: string[]; } @@ -24,8 +24,10 @@ export interface EuiSearchBoxProps extends SearchBoxConfigProps { title?: string; } +type DefaultProps = Pick; + export class EuiSearchBox extends Component { - static defaultProps: Partial = { + static defaultProps: DefaultProps = { placeholder: 'Search...', incremental: false, }; From 5620510327b7d43a521167eae1de900dd9d33328 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Sun, 1 Mar 2020 16:12:35 +0000 Subject: [PATCH 07/10] Remove .ignore --- .ignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .ignore diff --git a/.ignore b/.ignore deleted file mode 100644 index d8f8d46921a..00000000000 --- a/.ignore +++ /dev/null @@ -1 +0,0 @@ -docs From f9e1d5ed74dfec9ba0d637b4e6bec3d17b31d8e6 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Sun, 1 Mar 2020 16:30:16 +0000 Subject: [PATCH 08/10] Make defaultProps types explicit --- .../search_bar/filters/field_value_selection_filter.tsx | 4 +++- src/components/search_bar/search_filters.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/search_bar/filters/field_value_selection_filter.tsx b/src/components/search_bar/filters/field_value_selection_filter.tsx index 70d4c8c46b5..13373a72466 100644 --- a/src/components/search_bar/filters/field_value_selection_filter.tsx +++ b/src/components/search_bar/filters/field_value_selection_filter.tsx @@ -69,11 +69,13 @@ interface State { cachedOptions?: FieldValueOptionType[] | null; } +type DefaultProps = Pick; + export class FieldValueSelectionFilter extends Component< FieldValueSelectionFilterProps, State > { - static defaultProps: Partial = { + static defaultProps: DefaultProps = { autoClose: true, }; diff --git a/src/components/search_bar/search_filters.tsx b/src/components/search_bar/search_filters.tsx index 71478b12df9..88576b631ee 100644 --- a/src/components/search_bar/search_filters.tsx +++ b/src/components/search_bar/search_filters.tsx @@ -11,8 +11,10 @@ interface EuiSearchFiltersProps { filters: SearchFiltersFiltersType; } +type DefaultProps = Pick; + export class EuiSearchFilters extends Component { - static defaultProps: Partial = { + static defaultProps: DefaultProps = { filters: [], }; From 3eae2dab1397af5aeb317fda10b85a1d18bc21bd Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 5 Mar 2020 11:37:37 +0000 Subject: [PATCH 09/10] Use types to enforce onChange argument contract --- .../basic_table/in_memory_table.tsx | 17 +++++++++---- src/components/search_bar/search_bar.tsx | 24 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index 6dca1b40ac0..37ce69bd15d 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -331,11 +331,18 @@ export class EuiInMemoryTable extends Component< const { search } = this.props; if (isEuiSearchBarProps(search)) { if (search.onChange) { - const shouldQueryInMemory = search.onChange({ - query, - queryText, - error, - }); + const shouldQueryInMemory = + error == null + ? search.onChange({ + query: query!, + queryText, + error: null, + }) + : search.onChange({ + query: null, + queryText, + error, + }); if (!shouldQueryInMemory) { return; } diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 3f7d1789ac6..f00ff1aeb35 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -12,12 +12,20 @@ export type QueryType = Query | string; type Tools = ReactElement | ReactElement[]; +interface ArgsWithQuery { + query: Query; + queryText: string; + error: null; +} + +interface ArgsWithError { + query: null; + queryText: string; + error: Error; +} + export interface EuiSearchBarProps extends CommonProps { - onChange: (args: { - query: Query | null; - queryText: string; - error: Error | null; - }) => void | boolean; + onChange?: (args: ArgsWithQuery | ArgsWithError) => void | boolean; /** The initial query the bar will hold when first mounted @@ -133,7 +141,11 @@ export class EuiSearchBar extends Component { const isErrorDifferent = oldError !== newError; if (isQueryDifferent || isErrorDifferent) { - onChange({ query, queryText, error }); + if (error == null) { + onChange({ query: query!, queryText, error }); + } else { + onChange({ query: null, queryText, error }); + } } } From f2f4853c751613b6e5c91ea2b33ed856d251dfd9 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Fri, 6 Mar 2020 14:40:04 +0000 Subject: [PATCH 10/10] Make _AST public, define Syntax type, export EuiSearchBarProps --- src/components/search_bar/index.ts | 8 +++++++- src/components/search_bar/query/default_syntax.ts | 8 +++++++- src/components/search_bar/query/query.ts | 15 ++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/search_bar/index.ts b/src/components/search_bar/index.ts index 919d9f5333e..394c5df78b2 100644 --- a/src/components/search_bar/index.ts +++ b/src/components/search_bar/index.ts @@ -1,3 +1,9 @@ -export { EuiSearchBar, QueryType, Query, Ast } from './search_bar'; +export { + EuiSearchBar, + EuiSearchBarProps, + QueryType, + Query, + Ast, +} from './search_bar'; export { SearchBoxConfigProps } from './search_box'; export { SearchFiltersFiltersType } from './search_filters'; diff --git a/src/components/search_bar/query/default_syntax.ts b/src/components/search_bar/query/default_syntax.ts index 3f144e42c5b..605721ce7dc 100644 --- a/src/components/search_bar/query/default_syntax.ts +++ b/src/components/search_bar/query/default_syntax.ts @@ -461,7 +461,13 @@ const resolveOperator = (operator: OperatorType) => { } }; -export const defaultSyntax = Object.freeze({ +export type Syntax = Readonly<{ + printClause: (clause: Clause, text: string, options: any) => string; + print: (ast: _AST, options?: {}) => string; + parse: (query: string, options?: ParseOptions) => _AST; +}>; + +export const defaultSyntax: Syntax = Object.freeze({ parse: (query: string, options: ParseOptions = {}) => { const dateFormat = options.dateFormat || defaultDateFormat; const parseDate = dateValueParser(dateFormat); diff --git a/src/components/search_bar/query/query.ts b/src/components/search_bar/query/query.ts index 03e81dee5b2..89ab6bcfc6e 100644 --- a/src/components/search_bar/query/query.ts +++ b/src/components/search_bar/query/query.ts @@ -1,4 +1,4 @@ -import { defaultSyntax, ParseOptions } from './default_syntax'; +import { defaultSyntax, ParseOptions, Syntax } from './default_syntax'; import { executeAst } from './execute_ast'; import { isNil, isString } from '../../../services/predicate'; import { astToEsQueryDsl } from './ast_to_es_query_dsl'; @@ -11,7 +11,11 @@ import { _AST, AST, Clause, Operator, OperatorType, Value } from './ast'; * It is immutable - all mutating operations return a new (mutated) query instance. */ export class Query { - static parse(text: string, options?: ParseOptions, syntax = defaultSyntax) { + static parse( + text: string, + options?: ParseOptions, + syntax: Syntax = defaultSyntax + ) { return new Query(syntax.parse(text, options), syntax, text); } @@ -33,11 +37,12 @@ export class Query { return AST.Field.isInstance(clause); } - private ast: _AST; + // This ought to be `private`, but Kibana has some customizations that rely on access to this field + public ast: _AST; public text: string; - private syntax: any; + private syntax: Syntax; - constructor(ast: _AST, syntax = defaultSyntax, text?: string) { + constructor(ast: _AST, syntax: Syntax = defaultSyntax, text?: string) { this.ast = ast; this.text = text || syntax.print(ast); this.syntax = syntax;