mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
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:
parent
8291e1c7d0
commit
2a3cf660ab
17 changed files with 949 additions and 41 deletions
|
@ -18,3 +18,6 @@ export * from './src/get_operators';
|
||||||
export * from './src/hooks';
|
export * from './src/hooks';
|
||||||
export * from './src/operator';
|
export * from './src/operator';
|
||||||
export * from './src/param_is_valid';
|
export * from './src/param_is_valid';
|
||||||
|
export * from './src/param_contains_space';
|
||||||
|
|
||||||
|
export { default as autoCompletei18n } from './src/translations';
|
||||||
|
|
|
@ -8,15 +8,22 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactWrapper, mount } from 'enzyme';
|
import { ReactWrapper, mount } from 'enzyme';
|
||||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiSuperSelect } from '@elastic/eui';
|
import {
|
||||||
import { act } from '@testing-library/react';
|
EuiComboBox,
|
||||||
|
EuiComboBoxOptionOption,
|
||||||
|
EuiFormHelpText,
|
||||||
|
EuiSuperSelect,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { act, waitFor } from '@testing-library/react';
|
||||||
import { AutocompleteFieldMatchComponent } from '.';
|
import { AutocompleteFieldMatchComponent } from '.';
|
||||||
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
||||||
import { fields, getField } from '../fields/index.mock';
|
import { fields, getField } from '../fields/index.mock';
|
||||||
import { autocompleteStartMock } from '../autocomplete/index.mock';
|
import { autocompleteStartMock } from '../autocomplete/index.mock';
|
||||||
|
|
||||||
jest.mock('../hooks/use_field_value_autocomplete');
|
jest.mock('../hooks/use_field_value_autocomplete');
|
||||||
|
jest.mock('../translations', () => ({
|
||||||
|
FIELD_SPACE_WARNING: 'Warning: there is a space',
|
||||||
|
}));
|
||||||
describe('AutocompleteFieldMatchComponent', () => {
|
describe('AutocompleteFieldMatchComponent', () => {
|
||||||
let wrapper: ReactWrapper;
|
let wrapper: ReactWrapper;
|
||||||
|
|
||||||
|
@ -227,6 +234,48 @@ describe('AutocompleteFieldMatchComponent', () => {
|
||||||
expect(mockOnChange).toHaveBeenCalledWith('value 1');
|
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', () => {
|
test('it refreshes autocomplete with search query when new value searched', () => {
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<AutocompleteFieldMatchComponent
|
<AutocompleteFieldMatchComponent
|
||||||
|
@ -267,6 +316,83 @@ describe('AutocompleteFieldMatchComponent', () => {
|
||||||
selectedField: getField('machine.os.raw'),
|
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', () => {
|
describe('boolean type', () => {
|
||||||
const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]);
|
const valueSuggestionsMock = jest.fn().mockResolvedValue([false, false, [], jest.fn()]);
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
GetGenericComboBoxPropsReturn,
|
GetGenericComboBoxPropsReturn,
|
||||||
} from '../get_generic_combo_box_props';
|
} from '../get_generic_combo_box_props';
|
||||||
import { paramIsValid } from '../param_is_valid';
|
import { paramIsValid } from '../param_is_valid';
|
||||||
|
import { paramContainsSpace } from '../param_contains_space';
|
||||||
|
|
||||||
const BOOLEAN_OPTIONS = [
|
const BOOLEAN_OPTIONS = [
|
||||||
{ inputDisplay: 'true', value: 'true' },
|
{ inputDisplay: 'true', value: 'true' },
|
||||||
|
@ -66,13 +67,14 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
isClearable = false,
|
isClearable = false,
|
||||||
isRequired = false,
|
isRequired = false,
|
||||||
fieldInputWidth,
|
fieldInputWidth,
|
||||||
|
autocompleteService,
|
||||||
onChange,
|
onChange,
|
||||||
onError,
|
onError,
|
||||||
autocompleteService,
|
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [touched, setIsTouched] = useState(false);
|
const [touched, setIsTouched] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
|
||||||
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
|
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
|
||||||
autocompleteService,
|
autocompleteService,
|
||||||
fieldValue: selectedValue,
|
fieldValue: selectedValue,
|
||||||
|
@ -82,17 +84,27 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
selectedField,
|
selectedField,
|
||||||
});
|
});
|
||||||
const getLabel = useCallback((option: string): string => option, []);
|
const getLabel = useCallback((option: string): string => option, []);
|
||||||
|
|
||||||
const optionsMemo = useMemo((): string[] => {
|
const optionsMemo = useMemo((): string[] => {
|
||||||
const valueAsStr = String(selectedValue);
|
const valueAsStr = String(selectedValue);
|
||||||
return selectedValue != null && selectedValue.trim() !== ''
|
return selectedValue != null && selectedValue.trim() !== ''
|
||||||
? uniq([valueAsStr, ...suggestions])
|
? uniq([valueAsStr, ...suggestions])
|
||||||
: suggestions;
|
: suggestions;
|
||||||
}, [suggestions, selectedValue]);
|
}, [suggestions, selectedValue]);
|
||||||
|
|
||||||
const selectedOptionsMemo = useMemo((): string[] => {
|
const selectedOptionsMemo = useMemo((): string[] => {
|
||||||
const valueAsStr = String(selectedValue);
|
const valueAsStr = String(selectedValue);
|
||||||
return selectedValue ? [valueAsStr] : [];
|
return selectedValue ? [valueAsStr] : [];
|
||||||
}, [selectedValue]);
|
}, [selectedValue]);
|
||||||
|
|
||||||
|
const handleSpacesWarning = useCallback(
|
||||||
|
(param: string | undefined) => {
|
||||||
|
if (!param) return setShowSpacesWarning(false);
|
||||||
|
setShowSpacesWarning(!!paramContainsSpace(param));
|
||||||
|
},
|
||||||
|
[setShowSpacesWarning]
|
||||||
|
);
|
||||||
|
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
(err: string | undefined): void => {
|
(err: string | undefined): void => {
|
||||||
setError((existingErr): string | undefined => {
|
setError((existingErr): string | undefined => {
|
||||||
|
@ -121,10 +133,12 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
const handleValuesChange = useCallback(
|
const handleValuesChange = useCallback(
|
||||||
(newOptions: EuiComboBoxOptionOption[]): void => {
|
(newOptions: EuiComboBoxOptionOption[]): void => {
|
||||||
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
||||||
|
|
||||||
|
handleSpacesWarning(newValue);
|
||||||
handleError(undefined);
|
handleError(undefined);
|
||||||
onChange(newValue ?? '');
|
onChange(newValue ?? '');
|
||||||
},
|
},
|
||||||
[handleError, labels, onChange, optionsMemo]
|
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchChange = useCallback(
|
const handleSearchChange = useCallback(
|
||||||
|
@ -133,10 +147,11 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
||||||
handleError(err);
|
handleError(err);
|
||||||
|
|
||||||
|
if (!err) handleSpacesWarning(searchVal);
|
||||||
setSearchQuery(searchVal);
|
setSearchQuery(searchVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleError, isRequired, selectedField, touched]
|
[handleError, handleSpacesWarning, isRequired, selectedField, touched]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateOption = useCallback(
|
const handleCreateOption = useCallback(
|
||||||
|
@ -146,13 +161,15 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
// Explicitly reject the user's input
|
// Explicitly reject the user's input
|
||||||
|
setShowSpacesWarning(false);
|
||||||
return 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(
|
const handleNonComboBoxInputChange = useCallback(
|
||||||
|
@ -194,10 +211,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
if (onError != null) {
|
if (onError != null) onError(false);
|
||||||
onError(false);
|
|
||||||
}
|
handleSpacesWarning(selectedValue);
|
||||||
}, [selectedField, onError]);
|
}, [selectedField, selectedValue, handleSpacesWarning, onError]);
|
||||||
|
|
||||||
const defaultInput = useMemo((): JSX.Element => {
|
const defaultInput = useMemo((): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
@ -207,6 +224,7 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
isInvalid={selectedField != null && error != null}
|
isInvalid={selectedField != null && error != null}
|
||||||
data-test-subj="valuesAutocompleteMatchLabel"
|
data-test-subj="valuesAutocompleteMatchLabel"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
helpText={showSpacesWarning && i18n.FIELD_SPACE_WARNING}
|
||||||
>
|
>
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
|
@ -233,9 +251,6 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
comboOptions,
|
comboOptions,
|
||||||
error,
|
error,
|
||||||
fieldInputWidth,
|
fieldInputWidth,
|
||||||
handleCreateOption,
|
|
||||||
handleSearchChange,
|
|
||||||
handleValuesChange,
|
|
||||||
inputPlaceholder,
|
inputPlaceholder,
|
||||||
isClearable,
|
isClearable,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
@ -243,6 +258,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
|
||||||
rowLabel,
|
rowLabel,
|
||||||
selectedComboOptions,
|
selectedComboOptions,
|
||||||
selectedField,
|
selectedField,
|
||||||
|
showSpacesWarning,
|
||||||
|
handleCreateOption,
|
||||||
|
handleSearchChange,
|
||||||
|
handleValuesChange,
|
||||||
setIsTouchedValue,
|
setIsTouchedValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactWrapper, mount } from 'enzyme';
|
import { ReactWrapper, mount } from 'enzyme';
|
||||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormHelpText } from '@elastic/eui';
|
||||||
import { act } from '@testing-library/react';
|
import { act, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { AutocompleteFieldMatchAnyComponent } from '.';
|
import { AutocompleteFieldMatchAnyComponent } from '.';
|
||||||
import { getField, fields } from '../fields/index.mock';
|
import { getField, fields } from '../fields/index.mock';
|
||||||
|
@ -23,6 +23,9 @@ jest.mock('../hooks/use_field_value_autocomplete', () => {
|
||||||
useFieldValueAutocomplete: jest.fn(),
|
useFieldValueAutocomplete: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
jest.mock('../translations', () => ({
|
||||||
|
FIELD_SPACE_WARNING: 'Warning: there is a space',
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AutocompleteFieldMatchAnyComponent', () => {
|
describe('AutocompleteFieldMatchAnyComponent', () => {
|
||||||
let wrapper: ReactWrapper;
|
let wrapper: ReactWrapper;
|
||||||
|
@ -273,4 +276,128 @@ describe('AutocompleteFieldMatchAnyComponent', () => {
|
||||||
selectedField: getField('machine.os.raw'),
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* 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 { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
} from '../get_generic_combo_box_props';
|
} from '../get_generic_combo_box_props';
|
||||||
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
||||||
import { paramIsValid } from '../param_is_valid';
|
import { paramIsValid } from '../param_is_valid';
|
||||||
|
import { paramContainsSpace } from '../param_contains_space';
|
||||||
|
|
||||||
interface AutocompleteFieldMatchAnyProps {
|
interface AutocompleteFieldMatchAnyProps {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
@ -56,6 +57,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [touched, setIsTouched] = useState(false);
|
const [touched, setIsTouched] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
|
||||||
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
|
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
|
||||||
autocompleteService,
|
autocompleteService,
|
||||||
fieldValue: selectedValue,
|
fieldValue: selectedValue,
|
||||||
|
@ -78,7 +80,11 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
}),
|
}),
|
||||||
[optionsMemo, selectedValue, getLabel]
|
[optionsMemo, selectedValue, getLabel]
|
||||||
);
|
);
|
||||||
|
const handleSpacesWarning = useCallback(
|
||||||
|
(params: string[]) =>
|
||||||
|
setShowSpacesWarning(!!params.find((param: string) => paramContainsSpace(param))),
|
||||||
|
[setShowSpacesWarning]
|
||||||
|
);
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
(err: string | undefined): void => {
|
(err: string | undefined): void => {
|
||||||
setError((existingErr): string | undefined => {
|
setError((existingErr): string | undefined => {
|
||||||
|
@ -98,9 +104,10 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
(newOptions: EuiComboBoxOptionOption[]): void => {
|
(newOptions: EuiComboBoxOptionOption[]): void => {
|
||||||
const newValues: string[] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
const newValues: string[] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
||||||
handleError(undefined);
|
handleError(undefined);
|
||||||
|
handleSpacesWarning(newValues);
|
||||||
onChange(newValues);
|
onChange(newValues);
|
||||||
},
|
},
|
||||||
[handleError, labels, onChange, optionsMemo]
|
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchChange = useCallback(
|
const handleSearchChange = useCallback(
|
||||||
|
@ -113,10 +120,12 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
||||||
handleError(err);
|
handleError(err);
|
||||||
|
|
||||||
|
if (!err) handleSpacesWarning([searchVal]);
|
||||||
|
|
||||||
setSearchQuery(searchVal);
|
setSearchQuery(searchVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleError, isRequired, selectedField, touched]
|
[handleError, handleSpacesWarning, isRequired, selectedField, touched]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateOption = useCallback(
|
const handleCreateOption = useCallback(
|
||||||
|
@ -126,13 +135,15 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
// Explicitly reject the user's input
|
// Explicitly reject the user's input
|
||||||
|
setShowSpacesWarning(false);
|
||||||
return 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 => {
|
const setIsTouchedValue = useCallback((): void => {
|
||||||
|
@ -149,6 +160,9 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
(): boolean => isLoading || isLoadingSuggestions,
|
(): boolean => isLoading || isLoadingSuggestions,
|
||||||
[isLoading, isLoadingSuggestions]
|
[isLoading, isLoadingSuggestions]
|
||||||
);
|
);
|
||||||
|
useEffect((): void => {
|
||||||
|
handleSpacesWarning(selectedValue);
|
||||||
|
}, [selectedField, selectedValue, handleSpacesWarning]);
|
||||||
|
|
||||||
const defaultInput = useMemo((): JSX.Element => {
|
const defaultInput = useMemo((): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
@ -156,6 +170,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
label={rowLabel}
|
label={rowLabel}
|
||||||
error={error}
|
error={error}
|
||||||
isInvalid={selectedField != null && error != null}
|
isInvalid={selectedField != null && error != null}
|
||||||
|
helpText={showSpacesWarning && i18n.FIELD_SPACE_WARNING}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
|
@ -189,6 +204,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
|
||||||
rowLabel,
|
rowLabel,
|
||||||
selectedComboOptions,
|
selectedComboOptions,
|
||||||
selectedField,
|
selectedField,
|
||||||
|
showSpacesWarning,
|
||||||
setIsTouchedValue,
|
setIsTouchedValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactWrapper, mount } from 'enzyme';
|
import { ReactWrapper, mount } from 'enzyme';
|
||||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormHelpText } from '@elastic/eui';
|
||||||
import { act } from '@testing-library/react';
|
import { act, waitFor } from '@testing-library/react';
|
||||||
import { AutocompleteFieldWildcardComponent } from '.';
|
import { AutocompleteFieldWildcardComponent } from '.';
|
||||||
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
import { useFieldValueAutocomplete } from '../hooks/use_field_value_autocomplete';
|
||||||
import { fields, getField } from '../fields/index.mock';
|
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';
|
import { FILENAME_WILDCARD_WARNING, FILEPATH_WARNING } from '@kbn/securitysolution-utils';
|
||||||
|
|
||||||
jest.mock('../hooks/use_field_value_autocomplete');
|
jest.mock('../hooks/use_field_value_autocomplete');
|
||||||
|
jest.mock('../translations', () => ({
|
||||||
|
FIELD_SPACE_WARNING: 'Warning: there is a space',
|
||||||
|
}));
|
||||||
describe('AutocompleteFieldWildcardComponent', () => {
|
describe('AutocompleteFieldWildcardComponent', () => {
|
||||||
let wrapper: ReactWrapper;
|
let wrapper: ReactWrapper;
|
||||||
|
|
||||||
|
@ -389,4 +391,129 @@ describe('AutocompleteFieldWildcardComponent', () => {
|
||||||
expect(helpText.text()).toEqual(FILENAME_WILDCARD_WARNING);
|
expect(helpText.text()).toEqual(FILENAME_WILDCARD_WARNING);
|
||||||
expect(helpText.find('.euiToolTipAnchor')).toBeTruthy();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
GetGenericComboBoxPropsReturn,
|
GetGenericComboBoxPropsReturn,
|
||||||
} from '../get_generic_combo_box_props';
|
} from '../get_generic_combo_box_props';
|
||||||
import { paramIsValid } from '../param_is_valid';
|
import { paramIsValid } from '../param_is_valid';
|
||||||
|
import { paramContainsSpace } from '../param_contains_space';
|
||||||
|
|
||||||
const SINGLE_SELECTION = { asPlainText: true };
|
const SINGLE_SELECTION = { asPlainText: true };
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [touched, setIsTouched] = useState(false);
|
const [touched, setIsTouched] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
const [showSpacesWarning, setShowSpacesWarning] = useState<boolean>(false);
|
||||||
const [isLoadingSuggestions, , suggestions] = useFieldValueAutocomplete({
|
const [isLoadingSuggestions, , suggestions] = useFieldValueAutocomplete({
|
||||||
autocompleteService,
|
autocompleteService,
|
||||||
fieldValue: selectedValue,
|
fieldValue: selectedValue,
|
||||||
|
@ -88,6 +90,13 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
return selectedValue ? [valueAsStr] : [];
|
return selectedValue ? [valueAsStr] : [];
|
||||||
}, [selectedValue]);
|
}, [selectedValue]);
|
||||||
|
|
||||||
|
const handleSpacesWarning = useCallback(
|
||||||
|
(param: string | undefined) => {
|
||||||
|
if (!param) return setShowSpacesWarning(false);
|
||||||
|
setShowSpacesWarning(!!paramContainsSpace(param));
|
||||||
|
},
|
||||||
|
[setShowSpacesWarning]
|
||||||
|
);
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
(err: string | undefined): void => {
|
(err: string | undefined): void => {
|
||||||
setError((existingErr): string | undefined => {
|
setError((existingErr): string | undefined => {
|
||||||
|
@ -124,10 +133,12 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
(newOptions: EuiComboBoxOptionOption[]): void => {
|
(newOptions: EuiComboBoxOptionOption[]): void => {
|
||||||
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
|
||||||
handleError(undefined);
|
handleError(undefined);
|
||||||
handleWarning(undefined);
|
handleSpacesWarning(newValue);
|
||||||
|
setShowSpacesWarning(false);
|
||||||
|
|
||||||
onChange(newValue ?? '');
|
onChange(newValue ?? '');
|
||||||
},
|
},
|
||||||
[handleError, handleWarning, labels, onChange, optionsMemo]
|
[handleError, handleSpacesWarning, labels, onChange, optionsMemo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchChange = useCallback(
|
const handleSearchChange = useCallback(
|
||||||
|
@ -136,10 +147,12 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
|
||||||
handleError(err);
|
handleError(err);
|
||||||
handleWarning(warning);
|
handleWarning(warning);
|
||||||
|
if (!err) handleSpacesWarning(searchVal);
|
||||||
|
|
||||||
setSearchQuery(searchVal);
|
setSearchQuery(searchVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleError, isRequired, selectedField, touched, warning, handleWarning]
|
[handleError, handleSpacesWarning, isRequired, selectedField, touched, warning, handleWarning]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateOption = useCallback(
|
const handleCreateOption = useCallback(
|
||||||
|
@ -150,13 +163,24 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
// Explicitly reject the user's input
|
// Explicitly reject the user's input
|
||||||
|
setShowSpacesWarning(false);
|
||||||
return 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 => {
|
const setIsTouchedValue = useCallback((): void => {
|
||||||
|
@ -195,15 +219,17 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
if (onError != null) {
|
if (onError != null) {
|
||||||
onError(false);
|
onError(false);
|
||||||
}
|
}
|
||||||
|
handleSpacesWarning(selectedValue);
|
||||||
|
|
||||||
onWarning(false);
|
onWarning(false);
|
||||||
}, [selectedField, onError, onWarning]);
|
}, [selectedField, selectedValue, onError, onWarning, handleSpacesWarning]);
|
||||||
|
|
||||||
const defaultInput = useMemo((): JSX.Element => {
|
const defaultInput = useMemo((): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={rowLabel}
|
label={rowLabel}
|
||||||
error={error}
|
error={error}
|
||||||
helpText={warning}
|
helpText={warning || (showSpacesWarning && i18n.FIELD_SPACE_WARNING)}
|
||||||
isInvalid={selectedField != null && error != null}
|
isInvalid={selectedField != null && error != null}
|
||||||
data-test-subj="valuesAutocompleteWildcardLabel"
|
data-test-subj="valuesAutocompleteWildcardLabel"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -245,6 +271,7 @@ export const AutocompleteFieldWildcardComponent: React.FC<AutocompleteFieldWildc
|
||||||
selectedField,
|
selectedField,
|
||||||
setIsTouchedValue,
|
setIsTouchedValue,
|
||||||
warning,
|
warning,
|
||||||
|
showSpacesWarning,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return defaultInput;
|
return defaultInput;
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
|
@ -27,3 +27,17 @@ export const NUMBER_ERR = i18n.translate('autocomplete.invalidNumberError', {
|
||||||
export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', {
|
export const DATE_ERR = i18n.translate('autocomplete.invalidDateError', {
|
||||||
defaultMessage: 'Not a valid date',
|
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,
|
||||||
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ import type {
|
||||||
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||||
|
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
import { ValueWithSpaceWarning } from '../value_with_space_warning/value_with_space_warning';
|
||||||
|
|
||||||
const OS_LABELS = Object.freeze({
|
const OS_LABELS = Object.freeze({
|
||||||
linux: i18n.OS_LINUX,
|
linux: i18n.OS_LINUX,
|
||||||
|
@ -97,7 +98,6 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
|
||||||
return nestedEntries.map((entry) => {
|
return nestedEntries.map((entry) => {
|
||||||
const { field: nestedField, type: nestedType, operator: nestedOperator } = entry;
|
const { field: nestedField, type: nestedType, operator: nestedOperator } = entry;
|
||||||
const nestedValue = 'value' in entry ? entry.value : '';
|
const nestedValue = 'value' in entry ? entry.value : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroupNested
|
<EuiFlexGroupNested
|
||||||
data-test-subj={`${dataTestSubj}-nestedCondition`}
|
data-test-subj={`${dataTestSubj}-nestedCondition`}
|
||||||
|
@ -119,6 +119,7 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
|
||||||
value={getEntryValue(nestedType, nestedValue)}
|
value={getEntryValue(nestedType, nestedValue)}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItemNested>
|
</EuiFlexItemNested>
|
||||||
|
<ValueWithSpaceWarning value={nestedValue} />
|
||||||
</EuiFlexGroupNested>
|
</EuiFlexGroupNested>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -159,9 +160,9 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
|
||||||
{entries.map((entry, index) => {
|
{entries.map((entry, index) => {
|
||||||
const { field, type } = entry;
|
const { field, type } = entry;
|
||||||
const value = getValue(entry);
|
const value = getValue(entry);
|
||||||
|
|
||||||
const nestedEntries = 'entries' in entry ? entry.entries : [];
|
const nestedEntries = 'entries' in entry ? entry.entries : [];
|
||||||
const operator = 'operator' in entry ? entry.operator : '';
|
const operator = 'operator' in entry ? entry.operator : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test-subj={`${dataTestSubj}-condition`} key={field + type + value + index}>
|
<div data-test-subj={`${dataTestSubj}-condition`} key={field + type + value + index}>
|
||||||
<div className="eui-xScroll">
|
<div className="eui-xScroll">
|
||||||
|
@ -176,6 +177,7 @@ export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>(
|
||||||
description={getEntryOperator(type, operator)}
|
description={getEntryOperator(type, operator)}
|
||||||
value={getEntryValue(type, value)}
|
value={getEntryValue(type, value)}
|
||||||
/>
|
/>
|
||||||
|
<ValueWithSpaceWarning value={value} />
|
||||||
</div>
|
</div>
|
||||||
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)}
|
{nestedEntries != null && getNestedEntriesContent(type, nestedEntries)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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],
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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';
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue