diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 7db2d10444..7a75742a9c 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -2256,3 +2256,6 @@ export const MODULE_SCA_CHECK_RESULT_LABEL = { export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT = 30; // This limits the suggestions for the token of type value displayed in the search bar export const SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT = 10; +/* Time in milliseconds to debounce the analysis of search bar. This mitigates some problems related +to changes running in parallel */ +export const SEARCH_BAR_DEBOUNCE_UPDATE_TIME = 400; diff --git a/plugins/main/public/components/search-bar/index.tsx b/plugins/main/public/components/search-bar/index.tsx index 4a82d5d360..f73c4a5207 100644 --- a/plugins/main/public/components/search-bar/index.tsx +++ b/plugins/main/public/components/search-bar/index.tsx @@ -9,21 +9,22 @@ import { EuiSelect, EuiText, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, } from '@elastic/eui'; import { EuiSuggest } from '../eui-suggest'; import { searchBarQueryLanguages } from './query-language'; import _ from 'lodash'; import { ISearchBarModeWQL } from './query-language/wql'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; -export interface SearchBarProps{ +export interface SearchBarProps { defaultMode?: string; modes: ISearchBarModeWQL[]; onChange?: (params: any) => void; onSearch: (params: any) => void; - buttonsRender?: () => React.ReactNode + buttonsRender?: () => React.ReactNode; input?: string; -}; +} export const SearchBar = ({ defaultMode, @@ -54,12 +55,16 @@ export const SearchBar = ({ output: undefined, }); // Cache the previous output - const queryLanguageOutputRunPreviousOutput = useRef(queryLanguageOutputRun.output); + const queryLanguageOutputRunPreviousOutput = useRef( + queryLanguageOutputRun.output, + ); // Controls when the suggestion popover is open/close const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = useState(false); // Reference to the input const inputRef = useRef(); + // Debounce update timer + const debounceUpdateSearchBarTimer = useRef(); // Handler when searching const _onSearch = (output: any) => { @@ -79,55 +84,69 @@ export const SearchBar = ({ } }; - const selectedQueryLanguageParameters = modes.find(({ id }) => id === queryLanguage.id); + const selectedQueryLanguageParameters = modes.find( + ({ id }) => id === queryLanguage.id, + ); useEffect(() => { // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use - rest.input && searchBarQueryLanguages[queryLanguage.id]?.transformInput && setInput( - searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( - rest.input, - { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - } - ), - ); + rest.input && + searchBarQueryLanguages[queryLanguage.id]?.transformInput && + setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( + rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + ), + ); }, [rest.input]); useEffect(() => { (async () => { // Set the query language output - const queryLanguageOutput = await searchBarQueryLanguages[queryLanguage.id].run(input, { - onSearch: _onSearch, - setInput, - closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), - openSuggestionPopover: () => setIsOpenSuggestionPopover(true), - setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), - setQueryLanguageOutput: setQueryLanguageOutputRun, - inputRef, - queryLanguage: { - configuration: queryLanguage.configuration, - parameters: selectedQueryLanguageParameters, - }, - }); - queryLanguageOutputRunPreviousOutput.current = { - ...queryLanguageOutputRun.output - }; - setQueryLanguageOutputRun(queryLanguageOutput); + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + // Debounce the updating of the search bar state + debounceUpdateSearchBarTimer.current = setTimeout(async () => { + const queryLanguageOutput = await searchBarQueryLanguages[ + queryLanguage.id + ].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output, + }; + setQueryLanguageOutputRun(queryLanguageOutput); + }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); })(); }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange - // Ensure the previous output is different to the new one - && !_.isEqual(queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current) - && onChange(queryLanguageOutputRun.output); + onChange && + // Ensure the previous output is different to the new one + !_.isEqual( + queryLanguageOutputRun.output, + queryLanguageOutputRunPreviousOutput.current, + ) && + onChange(queryLanguageOutputRun.output); }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => @@ -163,7 +182,7 @@ export const SearchBar = ({ closePopover={onQueryLanguagePopoverSwitch} > SYNTAX OPTIONS -
+
{searchBarQueryLanguages[queryLanguage.id].description} @@ -173,7 +192,8 @@ export const SearchBar = ({
) => { + onChange={( + event: React.ChangeEvent, + ) => { const queryLanguageID: string = event.target.value; setQueryLanguage({ id: queryLanguageID, @@ -217,13 +239,19 @@ export const SearchBar = ({ /> ); - return rest.buttonsRender || queryLanguageOutputRun.filterButtons - ? ( - - {searchBar} - {rest.buttonsRender && {rest.buttonsRender()}} - {queryLanguageOutputRun.filterButtons && {queryLanguageOutputRun.filterButtons}} - - ) - : searchBar; + return rest.buttonsRender || queryLanguageOutputRun.filterButtons ? ( + + {searchBar} + {rest.buttonsRender && ( + {rest.buttonsRender()} + )} + {queryLanguageOutputRun.filterButtons && ( + + {queryLanguageOutputRun.filterButtons} + + )} + + ) : ( + searchBar + ); }; diff --git a/plugins/main/public/components/search-bar/query-language/wql.test.tsx b/plugins/main/public/components/search-bar/query-language/wql.test.tsx index 4de5de790b..c911516610 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.test.tsx @@ -303,7 +303,7 @@ describe('Query language - WQL', () => { ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} - ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2)'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} `( 'click suggestion - WQL $WQL => $changedInput', async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 63216eaeaf..10b3e1b0ea 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -102,10 +102,14 @@ const suggestionMappingLanguageTokenType = { * @returns */ function mapSuggestionCreator(type: ITokenType) { - return function ({ ...params }) { + return function ({ label, ...params }) { return { type, ...params, + /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is + displayed in the console related to prop types + */ + ...(typeof label !== 'undefined' ? { label: String(label) } : {}), }; }; } @@ -1094,10 +1098,15 @@ export const WQL = { : item.label; } else { // add a whitespace for conjunction + // add a whitespace for grouping operator ) !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction') && + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && tokens.push({ type: 'whitespace', value: ' ',