Skip to content

Commit 7aa8565

Browse files
authored
[Security Solution][Exceptions] - Update UI exceptions builder nested logic (#72490) (#72983)
## Summary This PR is meant to update the exception builder logic to handle nested fields. If you're unfamiliar with nested fields, you can read up more on it [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html) and [here](#44554). It also does a bit of cleanup, so though it may look like a lot of changes, parts of it were just moving some things around.
1 parent bdc08d0 commit 7aa8565

31 files changed

+3027
-808
lines changed

x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,50 +52,46 @@ describe('AutocompleteFieldListsComponent', () => {
5252
selectedField={getField('ip')}
5353
selectedValue="some-list-id"
5454
isLoading={false}
55-
isClearable={false}
56-
isDisabled={true}
55+
isClearable={true}
56+
isDisabled
5757
onChange={jest.fn()}
5858
/>
5959
</ThemeProvider>
6060
);
6161

62-
await waitFor(() => {
63-
expect(
64-
wrapper
65-
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
66-
.prop('disabled')
67-
).toBeTruthy();
68-
});
62+
expect(
63+
wrapper
64+
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
65+
.prop('disabled')
66+
).toBeTruthy();
6967
});
7068

7169
test('it renders loading if "isLoading" is true', async () => {
7270
const wrapper = mount(
7371
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
7472
<AutocompleteFieldListsComponent
7573
placeholder="Placeholder text"
76-
selectedField={getField('ip')}
77-
selectedValue="some-list-id"
78-
isLoading={true}
74+
selectedField={getField('@tags')}
75+
selectedValue=""
76+
isLoading
7977
isClearable={false}
8078
isDisabled={false}
8179
onChange={jest.fn()}
8280
/>
8381
</ThemeProvider>
8482
);
8583

86-
await waitFor(() => {
84+
wrapper
85+
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
86+
.at(0)
87+
.simulate('click');
88+
expect(
8789
wrapper
88-
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
89-
.at(0)
90-
.simulate('click');
91-
expect(
92-
wrapper
93-
.find(
94-
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
95-
)
96-
.prop('isLoading')
97-
).toBeTruthy();
98-
});
90+
.find(
91+
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
92+
)
93+
.prop('isLoading')
94+
).toBeTruthy();
9995
});
10096

10197
test('it allows user to clear values if "isClearable" is true', async () => {
@@ -104,19 +100,19 @@ describe('AutocompleteFieldListsComponent', () => {
104100
<AutocompleteFieldListsComponent
105101
placeholder="Placeholder text"
106102
selectedField={getField('ip')}
107-
selectedValue="some-list-id"
103+
selectedValue=""
108104
isLoading={false}
109-
isClearable={true}
105+
isClearable={false}
110106
isDisabled={false}
111107
onChange={jest.fn()}
112108
/>
113109
</ThemeProvider>
114110
);
115111
expect(
116112
wrapper
117-
.find(`[data-test-subj="comboBoxInput"]`)
118-
.hasClass('euiComboBox__inputWrap-isClearable')
119-
).toBeTruthy();
113+
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
114+
.prop('options')
115+
).toEqual([{ label: 'some name' }]);
120116
});
121117

122118
test('it correctly displays lists that match the selected "keyword" field esType', () => {
@@ -210,19 +206,24 @@ describe('AutocompleteFieldListsComponent', () => {
210206
onChange: (a: EuiComboBoxOptionOption[]) => void;
211207
}).onChange([{ label: 'some name' }]);
212208

213-
expect(mockOnChange).toHaveBeenCalledWith({
214-
created_at: DATE_NOW,
215-
created_by: 'some user',
216-
description: 'some description',
217-
id: 'some-list-id',
218-
meta: {},
219-
name: 'some name',
220-
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
221-
type: 'ip',
222-
updated_at: DATE_NOW,
223-
updated_by: 'some user',
224-
version: VERSION,
225-
immutable: IMMUTABLE,
209+
await waitFor(() => {
210+
expect(mockOnChange).toHaveBeenCalledWith({
211+
created_at: DATE_NOW,
212+
created_by: 'some user',
213+
description: 'some description',
214+
id: 'some-list-id',
215+
meta: {},
216+
name: 'some name',
217+
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
218+
type: 'ip',
219+
updated_at: DATE_NOW,
220+
updated_by: 'some user',
221+
_version: undefined,
222+
version: VERSION,
223+
deserializer: undefined,
224+
serializer: undefined,
225+
immutable: IMMUTABLE,
226+
});
226227
});
227228
});
228229
});

x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
99
import { IFieldType } from '../../../../../../../src/plugins/data/common';
1010
import { useFindLists, ListSchema } from '../../../lists_plugin_deps';
1111
import { useKibana } from '../../../common/lib/kibana';
12-
import { getGenericComboBoxProps } from './helpers';
12+
import { getGenericComboBoxProps, paramIsValid } from './helpers';
1313

1414
interface AutocompleteFieldListsProps {
1515
placeholder: string;
@@ -75,6 +75,8 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
7575
[labels, optionsMemo, onChange]
7676
);
7777

78+
const setIsTouchedValue = useCallback(() => setIsTouched(true), [setIsTouched]);
79+
7880
useEffect(() => {
7981
if (result != null) {
8082
setLists(result.data);
@@ -91,17 +93,24 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
9193
}
9294
}, [selectedField, start, http]);
9395

96+
const isValid = useMemo(
97+
(): boolean => paramIsValid(selectedValue, selectedField, isRequired, touched),
98+
[selectedField, selectedValue, isRequired, touched]
99+
);
100+
101+
const isLoadingState = useMemo((): boolean => isLoading || loading, [isLoading, loading]);
102+
94103
return (
95104
<EuiComboBox
96105
placeholder={placeholder}
97106
isDisabled={isDisabled}
98-
isLoading={isLoading || loading}
107+
isLoading={isLoadingState}
99108
isClearable={isClearable}
100109
options={comboOptions}
101110
selectedOptions={selectedComboOptions}
102111
onChange={handleValuesChange}
103-
isInvalid={isRequired ? touched && (selectedValue == null || selectedValue === '') : false}
104-
onFocus={() => setIsTouched(true)}
112+
isInvalid={!isValid}
113+
onFocus={setIsTouchedValue}
105114
singleSelection={{ asPlainText: true }}
106115
sortMatchesBy="startsWith"
107116
data-test-subj="valuesAutocompleteComboBox listsComboxBox"

x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { uniq } from 'lodash';
99

1010
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
1111
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
12-
import { validateParams, getGenericComboBoxProps } from './helpers';
12+
import { paramIsValid, getGenericComboBoxProps } from './helpers';
1313
import { OperatorTypeEnum } from '../../../lists_plugin_deps';
1414
import { GetGenericComboBoxPropsReturn } from './types';
1515
import * as i18n from './translations';
@@ -82,25 +82,37 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
8282
});
8383
};
8484

85-
const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [
86-
selectedField,
87-
selectedValue,
85+
const isValid = useMemo(
86+
(): boolean => paramIsValid(selectedValue, selectedField, isRequired, touched),
87+
[selectedField, selectedValue, isRequired, touched]
88+
);
89+
90+
const setIsTouchedValue = useCallback((): void => setIsTouched(true), [setIsTouched]);
91+
92+
const inputPlaceholder = useMemo(
93+
(): string => (isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder),
94+
[isLoading, isLoadingSuggestions, placeholder]
95+
);
96+
97+
const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [
98+
isLoading,
99+
isLoadingSuggestions,
88100
]);
89101

90102
return (
91103
<EuiComboBox
92-
placeholder={isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder}
104+
placeholder={inputPlaceholder}
93105
isDisabled={isDisabled}
94-
isLoading={isLoading || isLoadingSuggestions}
106+
isLoading={isLoadingState}
95107
isClearable={isClearable}
96108
options={comboOptions}
97109
selectedOptions={selectedComboOptions}
98110
onChange={handleValuesChange}
99111
singleSelection={{ asPlainText: true }}
100112
onSearchChange={onSearchChange}
101113
onCreateOption={onChange}
102-
isInvalid={isRequired ? touched && !isValid : false}
103-
onFocus={() => setIsTouched(true)}
114+
isInvalid={!isValid}
115+
onFocus={setIsTouchedValue}
104116
sortMatchesBy="startsWith"
105117
data-test-subj="valuesAutocompleteComboBox matchComboxBox"
106118
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}

x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { uniq } from 'lodash';
99

1010
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
1111
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
12-
import { getGenericComboBoxProps, validateParams } from './helpers';
12+
import { getGenericComboBoxProps, paramIsValid } from './helpers';
1313
import { OperatorTypeEnum } from '../../../lists_plugin_deps';
1414
import { GetGenericComboBoxPropsReturn } from './types';
1515
import * as i18n from './translations';
@@ -78,25 +78,38 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
7878
const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]);
7979

8080
const isValid = useMemo((): boolean => {
81-
const areAnyInvalid = selectedComboOptions.filter(
82-
({ label }) => !validateParams(label, selectedField)
83-
);
84-
return areAnyInvalid.length === 0;
85-
}, [selectedComboOptions, selectedField]);
81+
const areAnyInvalid =
82+
selectedComboOptions.filter(
83+
({ label }) => !paramIsValid(label, selectedField, isRequired, touched)
84+
).length > 0;
85+
return !areAnyInvalid;
86+
}, [selectedComboOptions, selectedField, isRequired, touched]);
87+
88+
const setIsTouchedValue = useCallback((): void => setIsTouched(true), [setIsTouched]);
89+
90+
const inputPlaceholder = useMemo(
91+
(): string => (isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder),
92+
[isLoading, isLoadingSuggestions, placeholder]
93+
);
94+
95+
const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [
96+
isLoading,
97+
isLoadingSuggestions,
98+
]);
8699

87100
return (
88101
<EuiComboBox
89-
placeholder={isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder}
90-
isLoading={isLoading || isLoadingSuggestions}
102+
placeholder={inputPlaceholder}
103+
isLoading={isLoadingState}
91104
isClearable={isClearable}
92105
isDisabled={isDisabled}
93106
options={comboOptions}
94107
selectedOptions={selectedComboOptions}
95108
onChange={handleValuesChange}
96109
onSearchChange={onSearchChange}
97110
onCreateOption={onCreateOption}
98-
isInvalid={isRequired ? touched && (selectedValue.length === 0 || !isValid) : !isValid}
99-
onFocus={() => setIsTouched(true)}
111+
isInvalid={!isValid}
112+
onFocus={setIsTouchedValue}
100113
delimiter=", "
101114
data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"
102115
fullWidth

x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
existsOperator,
1515
doesNotExistOperator,
1616
} from './operators';
17-
import { getOperators, validateParams, getGenericComboBoxProps } from './helpers';
17+
import { getOperators, paramIsValid, getGenericComboBoxProps } from './helpers';
1818

1919
describe('helpers', () => {
2020
describe('#getOperators', () => {
@@ -53,27 +53,67 @@ describe('helpers', () => {
5353
});
5454
});
5555

56-
describe('#validateParams', () => {
57-
test('returns false if value is undefined', () => {
58-
const isValid = validateParams(undefined, getField('@timestamp'));
56+
describe('#paramIsValid', () => {
57+
test('returns false if value is undefined and "isRequired" nad "touched" are true', () => {
58+
const isValid = paramIsValid(undefined, getField('@timestamp'), true, true);
5959

6060
expect(isValid).toBeFalsy();
6161
});
6262

63-
test('returns false if value is empty string', () => {
64-
const isValid = validateParams('', getField('@timestamp'));
63+
test('returns true if value is undefined and "isRequired" is true but "touched" is false', () => {
64+
const isValid = paramIsValid(undefined, getField('@timestamp'), true, false);
6565

66-
expect(isValid).toBeFalsy();
66+
expect(isValid).toBeTruthy();
67+
});
68+
69+
test('returns true if value is undefined and "isRequired" is false', () => {
70+
const isValid = paramIsValid(undefined, getField('@timestamp'), false, false);
71+
72+
expect(isValid).toBeTruthy();
73+
});
74+
75+
test('returns false if value is empty string when "isRequired" is true and "touched" is false', () => {
76+
const isValid = paramIsValid('', getField('@timestamp'), true, false);
77+
78+
expect(isValid).toBeTruthy();
79+
});
80+
81+
test('returns true if value is empty string and "isRequired" is false', () => {
82+
const isValid = paramIsValid('', getField('@timestamp'), false, false);
83+
84+
expect(isValid).toBeTruthy();
6785
});
6886

69-
test('returns true if type is "date" and value is valid', () => {
70-
const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp'));
87+
test('returns true if type is "date" and value is valid and "isRequired" is false', () => {
88+
const isValid = paramIsValid(
89+
'1994-11-05T08:15:30-05:00',
90+
getField('@timestamp'),
91+
false,
92+
false
93+
);
7194

7295
expect(isValid).toBeTruthy();
7396
});
7497

75-
test('returns false if type is "date" and value is not valid', () => {
76-
const isValid = validateParams('1593478826', getField('@timestamp'));
98+
test('returns true if type is "date" and value is valid and "isRequired" is true', () => {
99+
const isValid = paramIsValid(
100+
'1994-11-05T08:15:30-05:00',
101+
getField('@timestamp'),
102+
true,
103+
false
104+
);
105+
106+
expect(isValid).toBeTruthy();
107+
});
108+
109+
test('returns false if type is "date" and value is not valid and "isRequired" is false', () => {
110+
const isValid = paramIsValid('1593478826', getField('@timestamp'), false, false);
111+
112+
expect(isValid).toBeFalsy();
113+
});
114+
115+
test('returns false if type is "date" and value is not valid and "isRequired" is true', () => {
116+
const isValid = paramIsValid('1593478826', getField('@timestamp'), true, true);
77117

78118
expect(isValid).toBeFalsy();
79119
});

0 commit comments

Comments
 (0)