Fixing leading or trailing whitespace in exception entries (#139617)

* add param_contains_space + handle spaces in match, match_any and field_value_wildcard + translations

* add warning text to onChange events as well + add test for match with snanpshot

* add warning text to onChange events as well + add test for match with snanpshotcd

* add tests and snapshots for match_any and value_wildcard

* remove snapshots

* show warning in nested condition

* add param_contains_space + handle spaces in match, match_any and field_value_wildcard + translations

* add warning text to onChange events as well + add test for match with snanpshot

* add tests and snapshots for match_any and value_wildcard

* remove snapshots

* show warning in nested condition

* add ValueWithSpaceWarning component + tests in Exeption viewer

* add ValueWithSpaceWarning component + tests in Exeption viewer

* return </EuiFlexItemNested>

* apply ux comments

* use themeprovider

* apply docs-team comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wafaa Nasr 2022-08-31 16:14:56 +02:00 committed by GitHub
parent 8291e1c7d0
commit 2a3cf660ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 949 additions and 41 deletions

View file

@ -18,3 +18,6 @@ export * from './src/get_operators';
export * from './src/hooks';
export * from './src/operator';
export * from './src/param_is_valid';
export * from './src/param_contains_space';
export { default as autoCompletei18n } from './src/translations';

View file

@ -8,15 +8,22 @@
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui';
import { act } from '@testing-library/react';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormHelpText,
EuiSuperSelect,
} from '@elastic/eui';
import { act, waitFor } from '@testing-library/react';
import { AutocompleteFieldMatchComponent } from '.';
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
import { fields, getField } from '../fields/index.mock';
import { autocompleteStartMock } from '../autocomplete/index.mock';
jest.mock('../hooks/use_field_value_autocomplete');
jest.mock('../translations', () => ({
FIELD_SPACE_WARNING: 'Warning: there is a space',
}));
describe('AutocompleteFieldMatchComponent', () => {
let wrapper: ReactWrapper;
@ -227,6 +234,48 @@ describe('AutocompleteFieldMatchComponent', () => {
expect(mockOnChange).toHaveBeenCalledWith('value 1');
});
test('should show the warning helper text if the new value contains spaces when change', async () => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
true,
[' value 1 ', 'value 2'],
getValueSuggestionsMock,
]);
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
rowLabel="Test"
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={mockOnChange}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue=""
/>
);
await waitFor(() =>
(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: ' value 1 ' }])
);
wrapper.update();
expect(mockOnChange).toHaveBeenCalledWith(' value 1 ');
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
});
test('it refreshes autocomplete with search query when new value searched', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
@ -267,6 +316,83 @@ describe('AutocompleteFieldMatchComponent', () => {
selectedField: getField('machine.os.raw'),
});
});
test('should show the warning helper text if the new value contains spaces when searching a new query', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue=""
/>
);
act(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onSearchChange: (a: string) => void;
}
).onSearchChange(' value 1');
});
wrapper.update();
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should show the warning helper text if selectedValue contains spaces when editing', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue=" leading and trailing space "
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should not show the warning helper text if selectedValue is falsy', () => {
wrapper = mount(
<AutocompleteFieldMatchComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue=""
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeFalsy();
});
describe('boolean type', () => {
const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]);

View file

@ -31,6 +31,7 @@ import {
GetGenericComboBoxPropsReturn,
} from '../get_generic_combo_box_props';
import { paramIsValid } from '../param_is_valid';
import { paramContainsSpace } from '../param_contains_space';
const BOOLEAN_OPTIONS = [
{ inputDisplay: 'true', value: 'true' },
@ -66,13 +67,14 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
isClearable = false,
isRequired = false,
fieldInputWidth,
autocompleteService,
onChange,
onError,
autocompleteService,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
autocompleteService,
fieldValue: selectedValue,
@ -82,17 +84,27 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
selectedField,
});
const getLabel = useCallback((option: string): string => option, []);
const optionsMemo = useMemo((): string[] => {
const valueAsStr = String(selectedValue);
return selectedValue != null && selectedValue.trim() !== ''
? uniq([valueAsStr, ...suggestions])
: suggestions;
}, [suggestions, selectedValue]);
const selectedOptionsMemo = useMemo((): string[] => {
const valueAsStr = String(selectedValue);
return selectedValue ? [valueAsStr] : [];
}, [selectedValue]);
const handleSpacesWarning = useCallback(
(param: string | undefined) => {
if (!param) return setShowSpacesWarning(false);
setShowSpacesWarning(!!paramContainsSpace(param));
},
[setShowSpacesWarning]
);
const handleError = useCallback(
(err: string | undefined): void => {
setError((existingErr): string | undefined => {
@ -121,10 +133,12 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
const handleValuesChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]): void => {
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
handleSpacesWarning(newValue);
handleError(undefined);
onChange(newValue ?? '');
},
[handleError, labels, onChange, optionsMemo]
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
);
const handleSearchChange = useCallback(
@ -133,10 +147,11 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
handleError(err);
if (!err) handleSpacesWarning(searchVal);
setSearchQuery(searchVal);
}
},
[handleError, isRequired, selectedField, touched]
[handleError, handleSpacesWarning, isRequired, selectedField, touched]
);
const handleCreateOption = useCallback(
@ -146,13 +161,15 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
if (err != null) {
// Explicitly reject the user's input
setShowSpacesWarning(false);
return false;
} else {
onChange(option);
return undefined;
}
handleSpacesWarning(option);
onChange(option);
return undefined;
},
[isRequired, onChange, selectedField, touched, handleError]
[isRequired, onChange, selectedField, touched, handleError, handleSpacesWarning]
);
const handleNonComboBoxInputChange = useCallback(
@ -194,10 +211,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
useEffect((): void => {
setError(undefined);
if (onError != null) {
onError(false);
}
}, [selectedField, onError]);
if (onError != null) onError(false);
handleSpacesWarning(selectedValue);
}, [selectedField, selectedValue, handleSpacesWarning, onError]);
const defaultInput = useMemo((): JSX.Element => {
return (
@ -207,6 +224,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
isInvalid={selectedField != null && error != null}
data-test-subj="valuesAutocompleteMatchLabel"
fullWidth
helpText={showSpacesWarning && i18n.FIELD_SPACE_WARNING}
>
<EuiComboBox
placeholder={inputPlaceholder}
@ -233,9 +251,6 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
comboOptions,
error,
fieldInputWidth,
handleCreateOption,
handleSearchChange,
handleValuesChange,
inputPlaceholder,
isClearable,
isDisabled,
@ -243,6 +258,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
rowLabel,
selectedComboOptions,
selectedField,
showSpacesWarning,
handleCreateOption,
handleSearchChange,
handleValuesChange,
setIsTouchedValue,
]);

View file

@ -8,8 +8,8 @@
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { act } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormHelpText } from '@elastic/eui';
import { act, waitFor } from '@testing-library/react';
import { AutocompleteFieldMatchAnyComponent } from '.';
import { getField, fields } from '../fields/index.mock';
@ -23,6 +23,9 @@ jest.mock('../hooks/use_field_value_autocomplete', () => {
useFieldValueAutocomplete: jest.fn(),
};
});
jest.mock('../translations', () => ({
FIELD_SPACE_WARNING: 'Warning: there is a space',
}));
describe('AutocompleteFieldMatchAnyComponent', () => {
let wrapper: ReactWrapper;
@ -273,4 +276,128 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
selectedField: getField('machine.os.raw'),
});
});
test('should show the warning helper text if the new value contains spaces when change', async () => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
true,
[' value 1 ', 'value 2'],
getValueSuggestionsMock,
]);
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
autocompleteService={{
...autocompleteStartMock,
}}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={mockOnChange}
placeholder="Placeholder text"
rowLabel={'Row Label'}
selectedField={getField('machine.os.raw')}
selectedValue={[]}
/>
);
await waitFor(() =>
(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: ' value 1 ' }])
);
wrapper.update();
expect(mockOnChange).toHaveBeenCalledWith([' value 1 ']);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
});
test('should show the warning helper text if the new value contains spaces when searching a new query', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
autocompleteService={{
...autocompleteStartMock,
}}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
placeholder="Placeholder text"
rowLabel={'Row Label'}
selectedField={getField('machine.os.raw')}
selectedValue={[]}
/>
);
act(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onSearchChange: (a: string) => void;
}
).onSearchChange(' value 1');
});
wrapper.update();
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should show the warning helper text if selectedValue contains spaces when editing', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
autocompleteService={{
...autocompleteStartMock,
}}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
placeholder="Placeholder text"
rowLabel={'Row Label'}
selectedField={getField('machine.os.raw')}
selectedValue={['value with trailing space ', 'value 1']}
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should not show the warning helper text if selectedValue is falsy', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
autocompleteService={{
...autocompleteStartMock,
}}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
placeholder="Placeholder text"
rowLabel={'Row Label'}
selectedField={getField('machine.os.raw')}
selectedValue={[]}
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeFalsy();
});
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { uniq } from 'lodash';
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
@ -23,6 +23,7 @@ import {
} from '../get_generic_combo_box_props';
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
import { paramIsValid } from '../param_is_valid';
import { paramContainsSpace } from '../param_contains_space';
interface AutocompleteFieldMatchAnyProps {
placeholder: string;
@ -56,6 +57,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
autocompleteService,
fieldValue: selectedValue,
@ -78,7 +80,11 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
}),
[optionsMemo, selectedValue, getLabel]
);
const handleSpacesWarning = useCallback(
(params: string[]) =>
setShowSpacesWarning(!!params.find((param: string) => paramContainsSpace(param))),
[setShowSpacesWarning]
);
const handleError = useCallback(
(err: string | undefined): void => {
setError((existingErr): string | undefined => {
@ -98,9 +104,10 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
(newOptions: EuiComboBoxOptionOption[]): void => {
const newValues: string[] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
handleError(undefined);
handleSpacesWarning(newValues);
onChange(newValues);
},
[handleError, labels, onChange, optionsMemo]
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
);
const handleSearchChange = useCallback(
@ -113,10 +120,12 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
handleError(err);
if (!err) handleSpacesWarning([searchVal]);
setSearchQuery(searchVal);
}
},
[handleError, isRequired, selectedField, touched]
[handleError, handleSpacesWarning, isRequired, selectedField, touched]
);
const handleCreateOption = useCallback(
@ -126,13 +135,15 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
if (err != null) {
// Explicitly reject the user's input
setShowSpacesWarning(false);
return false;
} else {
onChange([...(selectedValue || []), option]);
return true;
}
onChange([...(selectedValue || []), option]);
handleSpacesWarning([option]);
return true;
},
[handleError, isRequired, onChange, selectedField, selectedValue, touched]
[handleError, handleSpacesWarning, isRequired, onChange, selectedField, selectedValue, touched]
);
const setIsTouchedValue = useCallback((): void => {
@ -149,6 +160,9 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
(): boolean => isLoading || isLoadingSuggestions,
[isLoading, isLoadingSuggestions]
);
useEffect((): void => {
handleSpacesWarning(selectedValue);
}, [selectedField, selectedValue, handleSpacesWarning]);
const defaultInput = useMemo((): JSX.Element => {
return (
@ -156,6 +170,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
label={rowLabel}
error={error}
isInvalid={selectedField != null && error != null}
helpText={showSpacesWarning && i18n.FIELD_SPACE_WARNING}
fullWidth
>
<EuiComboBox
@ -189,6 +204,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
rowLabel,
selectedComboOptions,
selectedField,
showSpacesWarning,
setIsTouchedValue,
]);

View file

@ -8,8 +8,8 @@
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { act } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormHelpText } from '@elastic/eui';
import { act, waitFor } from '@testing-library/react';
import { AutocompleteFieldWildcardComponent } from '.';
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
import { fields, getField } from '../fields/index.mock';
@ -17,7 +17,9 @@ import { autocompleteStartMock } from '../autocomplete/index.mock';
import { FILENAME_WILDCARD_WARNING, FILEPATH_WARNING } from '@kbn/securitysolution-utils';
jest.mock('../hooks/use_field_value_autocomplete');
jest.mock('../translations', () => ({
FIELD_SPACE_WARNING: 'Warning: there is a space',
}));
describe('AutocompleteFieldWildcardComponent', () => {
let wrapper: ReactWrapper;
@ -389,4 +391,129 @@ describe('AutocompleteFieldWildcardComponent', () => {
expect(helpText.text()).toEqual(FILENAME_WILDCARD_WARNING);
expect(helpText.find('.euiToolTipAnchor')).toBeTruthy();
});
test('should show the warning helper text if the new value contains spaces when change', async () => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
true,
[' value 1 ', 'value 2'],
getValueSuggestionsMock,
]);
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldWildcardComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logs-endpoint.events.*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={mockOnChange}
onError={jest.fn()}
onWarning={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('file.path.text')}
selectedValue="invalid path"
warning={FILENAME_WILDCARD_WARNING}
/>
);
await waitFor(() =>
(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: ' value 1 ' }])
);
wrapper.update();
expect(mockOnChange).toHaveBeenCalledWith(' value 1 ');
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
});
test('should show the warning helper text if the new value contains spaces when searching a new query', () => {
wrapper = mount(
<AutocompleteFieldWildcardComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logs-endpoint.events.*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
onWarning={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('file.path.text')}
selectedValue="invalid path"
warning={''}
/>
);
act(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onSearchChange: (a: string) => void;
}
).onSearchChange(' value 1');
});
wrapper.update();
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should show the warning helper text if selectedValue contains spaces when editing', () => {
wrapper = mount(
<AutocompleteFieldWildcardComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logs-endpoint.events.*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
onWarning={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('file.path.text')}
selectedValue=" leading space"
warning={''}
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeTruthy();
expect(euiFormHelptext.text()).toEqual('Warning: there is a space');
});
test('should not show the warning helper text if selectedValue is falsy', () => {
wrapper = mount(
<AutocompleteFieldWildcardComponent
autocompleteService={autocompleteStartMock}
indexPattern={{
fields,
id: '1234',
title: 'logs-endpoint.events.*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
onError={jest.fn()}
onWarning={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('file.path.text')}
selectedValue=""
warning={''}
/>
);
const euiFormHelptext = wrapper.find(EuiFormHelpText);
expect(euiFormHelptext.length).toBeFalsy();
});
});

View file

@ -25,6 +25,7 @@ import {
GetGenericComboBoxPropsReturn,
} from '../get_generic_combo_box_props';
import { paramIsValid } from '../param_is_valid';
import { paramContainsSpace } from '../param_contains_space';
const SINGLE_SELECTION = { asPlainText: true };
@ -68,6 +69,7 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
const [isLoadingSuggestions, , suggestions] = useFieldValueAutocomplete({
autocompleteService,
fieldValue: selectedValue,
@ -88,6 +90,13 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
return selectedValue ? [valueAsStr] : [];
}, [selectedValue]);
const handleSpacesWarning = useCallback(
(param: string | undefined) => {
if (!param) return setShowSpacesWarning(false);
setShowSpacesWarning(!!paramContainsSpace(param));
},
[setShowSpacesWarning]
);
const handleError = useCallback(
(err: string | undefined): void => {
setError((existingErr): string | undefined => {
@ -124,10 +133,12 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
(newOptions: EuiComboBoxOptionOption[]): void => {
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
handleError(undefined);
handleWarning(undefined);
handleSpacesWarning(newValue);
setShowSpacesWarning(false);
onChange(newValue ?? '');
},
[handleError, handleWarning, labels, onChange, optionsMemo]
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
);
const handleSearchChange = useCallback(
@ -136,10 +147,12 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
handleError(err);
handleWarning(warning);
if (!err) handleSpacesWarning(searchVal);
setSearchQuery(searchVal);
}
},
[handleError, isRequired, selectedField, touched, warning, handleWarning]
[handleError, handleSpacesWarning, isRequired, selectedField, touched, warning, handleWarning]
);
const handleCreateOption = useCallback(
@ -150,13 +163,24 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
if (err != null) {
// Explicitly reject the user's input
setShowSpacesWarning(false);
return false;
} else {
onChange(option);
return undefined;
}
handleSpacesWarning(option);
onChange(option);
return undefined;
},
[isRequired, onChange, selectedField, touched, handleError, handleWarning, warning]
[
isRequired,
handleSpacesWarning,
onChange,
selectedField,
touched,
handleError,
handleWarning,
warning,
]
);
const setIsTouchedValue = useCallback((): void => {
@ -195,15 +219,17 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
if (onError != null) {
onError(false);
}
handleSpacesWarning(selectedValue);
onWarning(false);
}, [selectedField, onError, onWarning]);
}, [selectedField, selectedValue, onError, onWarning, handleSpacesWarning]);
const defaultInput = useMemo((): JSX.Element => {
return (
<EuiFormRow
label={rowLabel}
error={error}
helpText={warning}
helpText={warning || (showSpacesWarning && i18n.FIELD_SPACE_WARNING)}
isInvalid={selectedField != null && error != null}
data-test-subj="valuesAutocompleteWildcardLabel"
fullWidth
@ -245,6 +271,7 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
selectedField,
setIsTouchedValue,
warning,
showSpacesWarning,
]);
return defaultInput;

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { paramContainsSpace } from '.';
describe('param_contains_space', () => {
test('should return true if leading spaces were found', () => {
expect(paramContainsSpace(' test')).toBeTruthy();
});
test('should return true if trailing spaces were found', () => {
expect(paramContainsSpace('test ')).toBeTruthy();
});
test('should return true if both trailing and leading spaces were found', () => {
expect(paramContainsSpace(' test ')).toBeTruthy();
});
test('should return true if tabs was found', () => {
expect(paramContainsSpace('\ttest')).toBeTruthy();
});
test('should return false if no spaces were found', () => {
expect(paramContainsSpace('test test')).toBeFalsy();
});
test('should return false if param is falsy', () => {
expect(paramContainsSpace('')).toBeFalsy();
});
});

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const paramContainsSpace = (param: string) => param && param.trim().length !== param.length;

View file

@ -27,3 +27,17 @@ export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', {
export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', {
defaultMessage: 'Not a valid date',
});
export const FIELD_SPACE_WARNING = i18n.translate('autocomplete.fieldSpaceWarning', {
defaultMessage: "Warning: Spaces at the start or end of this value aren't being displayed.",
});
// eslint-disable-next-line import/no-default-export
export default {
LOADING,
SELECT_FIELD_FIRST,
FIELD_REQUIRED_ERR,
NUMBER_ERR,
DATE_ERR,
FIELD_SPACE_WARNING,
};

View file

@ -21,6 +21,7 @@ import type {
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import * as i18n from './translations';
import { ValueWithSpaceWarning } from '../value_with_space_warning/value_with_space_warning';
const OS_LABELS = Object.freeze({
linux: i18n.OS_LINUX,
@ -97,7 +98,6 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
return nestedEntries.map((entry) => {
const { field: nestedField, type: nestedType, operator: nestedOperator } = entry;
const nestedValue = 'value' in entry ? entry.value : '';
return (
<EuiFlexGroupNested
data-test-subj={`${dataTestSubj}-nestedCondition`}
@ -119,6 +119,7 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
value={getEntryValue(nestedType, nestedValue)}
/>
</EuiFlexItemNested>
<ValueWithSpaceWarning value={nestedValue} />
</EuiFlexGroupNested>
);
});
@ -159,9 +160,9 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
{entries.map((entry, index) => {
const { field, type } = entry;
const value = getValue(entry);
const nestedEntries = 'entries' in entry ? entry.entries : [];
const operator = 'operator' in entry ? entry.operator : '';
return (
<div data-test-subj={`${dataTestSubj}-condition`} key={field + type + value + index}>
<div className="eui-xScroll">
@ -176,6 +177,7 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
description={getEntryOperator(type, operator)}
value={getEntryValue(type, value)}
/>
<ValueWithSpaceWarning value={value} />
</div>
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)}
</div>

View file

@ -0,0 +1,217 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ValueWithSpaceWarning should not render if showSpaceWarning is falsy 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`ValueWithSpaceWarning should not render if value is falsy 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`ValueWithSpaceWarning should render if showSpaceWarning is truthy 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
.c0 {
display: inline;
margin-left: 4px;
}
<div>
<div
class="c0"
>
<span
class="euiToolTipAnchor"
>
<span
color="warning"
data-euiicon-type="iInCircle"
data-test-subj="value_with_space_warning_tooltip"
/>
</span>
</div>
</div>
</body>,
"container": <div>
<div
class="sc-AxjAm dmOCRD"
>
<span
class="euiToolTipAnchor"
>
<span
color="warning"
data-euiicon-type="iInCircle"
data-test-subj="value_with_space_warning_tooltip"
/>
</span>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useValueWithSpaceWarning } from '../use_value_with_space_warning';
describe('useValueWithSpaceWarning', () => {
it('should return true when value is string and contains space', () => {
const { result } = renderHook(() => useValueWithSpaceWarning({ value: ' space before' }));
const { showSpaceWarningIcon, warningText } = result.current;
expect(showSpaceWarningIcon).toBeTruthy();
expect(warningText).toBeTruthy();
});
it('should return true when value is string and does not contain space', () => {
const { result } = renderHook(() => useValueWithSpaceWarning({ value: 'no space' }));
const { showSpaceWarningIcon, warningText } = result.current;
expect(showSpaceWarningIcon).toBeFalsy();
expect(warningText).toBeTruthy();
});
it('should return true when value is array and one of the elements contains space', () => {
const { result } = renderHook(() =>
useValueWithSpaceWarning({ value: [' space before', 'no space'] })
);
const { showSpaceWarningIcon, warningText } = result.current;
expect(showSpaceWarningIcon).toBeTruthy();
expect(warningText).toBeTruthy();
});
it('should return true when value is array and none contains space', () => {
const { result } = renderHook(() =>
useValueWithSpaceWarning({ value: ['no space', 'no space'] })
);
const { showSpaceWarningIcon, warningText } = result.current;
expect(showSpaceWarningIcon).toBeFalsy();
expect(warningText).toBeTruthy();
});
it('should return the tooltipIconText', () => {
const { result } = renderHook(() =>
useValueWithSpaceWarning({ value: ' space before', tooltipIconText: 'Warning Text' })
);
const { showSpaceWarningIcon, warningText } = result.current;
expect(showSpaceWarningIcon).toBeTruthy();
expect(warningText).toEqual('Warning Text');
});
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { ValueWithSpaceWarning } from '..';
import * as useValueWithSpaceWarningMock from '../use_value_with_space_warning';
jest.mock('../use_value_with_space_warning');
describe('ValueWithSpaceWarning', () => {
beforeEach(() => {
// @ts-ignore
useValueWithSpaceWarningMock.useValueWithSpaceWarning = jest
.fn()
.mockReturnValue({ showSpaceWarningIcon: true, warningText: 'Warning Text' });
});
it('should not render if value is falsy', () => {
const container = render(<ValueWithSpaceWarning value="" />);
expect(container).toMatchSnapshot();
});
it('should not render if showSpaceWarning is falsy', () => {
// @ts-ignore
useValueWithSpaceWarningMock.useValueWithSpaceWarning = jest
.fn()
.mockReturnValue({ showSpaceWarningIcon: false, warningText: '' });
const container = render(<ValueWithSpaceWarning value="Test" />);
expect(container).toMatchSnapshot();
});
it('should render if showSpaceWarning is truthy', () => {
const container = render(
<ThemeProvider theme={() => ({ eui: { euiSizeXS: '4px' } })}>
<ValueWithSpaceWarning value="Test" />
</ThemeProvider>
);
expect(container).toMatchSnapshot();
});
it('should show the tooltip when the icon is clicked', async () => {
const container = render(
<ThemeProvider theme={() => ({ eui: { euiSizeXS: '4px' } })}>
<ValueWithSpaceWarning value="Test" />
</ThemeProvider>
);
fireEvent.mouseOver(container.getByTestId('value_with_space_warning_tooltip'));
expect(await container.findByText('Warning Text')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { ValueWithSpaceWarning } from './value_with_space_warning';

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { paramContainsSpace, autoCompletei18n } from '@kbn/securitysolution-autocomplete';
interface UseValueWithSpaceWarningResult {
showSpaceWarningIcon: boolean;
warningText: string;
}
interface UseValueWithSpaceWarningProps {
value: string | string[];
tooltipIconText?: string;
}
export const useValueWithSpaceWarning = ({
value,
tooltipIconText,
}: UseValueWithSpaceWarningProps): UseValueWithSpaceWarningResult => {
const showSpaceWarningIcon = Array.isArray(value)
? value.find(paramContainsSpace)
: paramContainsSpace(value);
return {
showSpaceWarningIcon: !!showSpaceWarningIcon,
warningText: tooltipIconText || autoCompletei18n.FIELD_SPACE_WARNING,
};
};

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { FC } from 'react';
import styled from 'styled-components';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { useValueWithSpaceWarning } from './use_value_with_space_warning';
interface ValueWithSpaceWarningProps {
value: string[] | string;
tooltipIconType?: string;
tooltipIconText?: string;
}
const Container = styled.div`
display: inline;
margin-left: ${({ theme }) => `${theme.eui.euiSizeXS}`};
`;
export const ValueWithSpaceWarning: FC<ValueWithSpaceWarningProps> = ({
value,
tooltipIconType = 'iInCircle',
tooltipIconText,
}) => {
const { showSpaceWarningIcon, warningText } = useValueWithSpaceWarning({
value,
tooltipIconText,
});
if (!showSpaceWarningIcon || !value) return null;
return (
<Container>
<EuiToolTip position="top" content={warningText}>
<EuiIcon
data-test-subj="value_with_space_warning_tooltip"
type={tooltipIconType}
color="warning"
/>
</EuiToolTip>
</Container>
);
};