Skip to content

Commit

Permalink
[Security Solution][Exceptions] - Update UI exceptions builder nested…
Browse files Browse the repository at this point in the history
… logic (#72490)

## 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.
  • Loading branch information
yctercero authored Jul 22, 2020
1 parent 1f155de commit f7a1679
Show file tree
Hide file tree
Showing 31 changed files with 3,027 additions and 808 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,50 +52,46 @@ describe('AutocompleteFieldListsComponent', () => {
selectedField={getField('ip')}
selectedValue="some-list-id"
isLoading={false}
isClearable={false}
isDisabled={true}
isClearable={true}
isDisabled
onChange={jest.fn()}
/>
</ThemeProvider>
);

await waitFor(() => {
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
});
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
});

test('it renders loading if "isLoading" is true', async () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
isLoading={true}
selectedField={getField('@tags')}
selectedValue=""
isLoading
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);

await waitFor(() => {
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
.at(0)
.simulate('click');
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
.at(0)
.simulate('click');
expect(
wrapper
.find(
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
)
.prop('isLoading')
).toBeTruthy();
});
.find(
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
)
.prop('isLoading')
).toBeTruthy();
});

test('it allows user to clear values if "isClearable" is true', async () => {
Expand All @@ -104,19 +100,19 @@ describe('AutocompleteFieldListsComponent', () => {
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
selectedValue=""
isLoading={false}
isClearable={true}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);
expect(
wrapper
.find(`[data-test-subj="comboBoxInput"]`)
.hasClass('euiComboBox__inputWrap-isClearable')
).toBeTruthy();
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'some name' }]);
});

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

expect(mockOnChange).toHaveBeenCalledWith({
created_at: DATE_NOW,
created_by: 'some user',
description: 'some description',
id: 'some-list-id',
meta: {},
name: 'some name',
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
type: 'ip',
updated_at: DATE_NOW,
updated_by: 'some user',
version: VERSION,
immutable: IMMUTABLE,
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith({
created_at: DATE_NOW,
created_by: 'some user',
description: 'some description',
id: 'some-list-id',
meta: {},
name: 'some name',
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
type: 'ip',
updated_at: DATE_NOW,
updated_by: 'some user',
_version: undefined,
version: VERSION,
deserializer: undefined,
serializer: undefined,
immutable: IMMUTABLE,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { useFindLists, ListSchema } from '../../../lists_plugin_deps';
import { useKibana } from '../../../common/lib/kibana';
import { getGenericComboBoxProps } from './helpers';
import { getGenericComboBoxProps, paramIsValid } from './helpers';

interface AutocompleteFieldListsProps {
placeholder: string;
Expand Down Expand Up @@ -75,6 +75,8 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
[labels, optionsMemo, onChange]
);

const setIsTouchedValue = useCallback(() => setIsTouched(true), [setIsTouched]);

useEffect(() => {
if (result != null) {
setLists(result.data);
Expand All @@ -91,17 +93,24 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
}
}, [selectedField, start, http]);

const isValid = useMemo(
(): boolean => paramIsValid(selectedValue, selectedField, isRequired, touched),
[selectedField, selectedValue, isRequired, touched]
);

const isLoadingState = useMemo((): boolean => isLoading || loading, [isLoading, loading]);

return (
<EuiComboBox
placeholder={placeholder}
isDisabled={isDisabled}
isLoading={isLoading || loading}
isLoading={isLoadingState}
isClearable={isClearable}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
isInvalid={isRequired ? touched && (selectedValue == null || selectedValue === '') : false}
onFocus={() => setIsTouched(true)}
isInvalid={!isValid}
onFocus={setIsTouchedValue}
singleSelection={{ asPlainText: true }}
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteComboBox listsComboxBox"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { uniq } from 'lodash';

import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
import { validateParams, getGenericComboBoxProps } from './helpers';
import { paramIsValid, getGenericComboBoxProps } from './helpers';
import { OperatorTypeEnum } from '../../../lists_plugin_deps';
import { GetGenericComboBoxPropsReturn } from './types';
import * as i18n from './translations';
Expand Down Expand Up @@ -82,25 +82,37 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
});
};

const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [
selectedField,
selectedValue,
const isValid = useMemo(
(): boolean => paramIsValid(selectedValue, selectedField, isRequired, touched),
[selectedField, selectedValue, isRequired, touched]
);

const setIsTouchedValue = useCallback((): void => setIsTouched(true), [setIsTouched]);

const inputPlaceholder = useMemo(
(): string => (isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder),
[isLoading, isLoadingSuggestions, placeholder]
);

const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [
isLoading,
isLoadingSuggestions,
]);

return (
<EuiComboBox
placeholder={isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder}
placeholder={inputPlaceholder}
isDisabled={isDisabled}
isLoading={isLoading || isLoadingSuggestions}
isLoading={isLoadingState}
isClearable={isClearable}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
singleSelection={{ asPlainText: true }}
onSearchChange={onSearchChange}
onCreateOption={onChange}
isInvalid={isRequired ? touched && !isValid : false}
onFocus={() => setIsTouched(true)}
isInvalid={!isValid}
onFocus={setIsTouchedValue}
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteComboBox matchComboxBox"
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { uniq } from 'lodash';

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

const isValid = useMemo((): boolean => {
const areAnyInvalid = selectedComboOptions.filter(
({ label }) => !validateParams(label, selectedField)
);
return areAnyInvalid.length === 0;
}, [selectedComboOptions, selectedField]);
const areAnyInvalid =
selectedComboOptions.filter(
({ label }) => !paramIsValid(label, selectedField, isRequired, touched)
).length > 0;
return !areAnyInvalid;
}, [selectedComboOptions, selectedField, isRequired, touched]);

const setIsTouchedValue = useCallback((): void => setIsTouched(true), [setIsTouched]);

const inputPlaceholder = useMemo(
(): string => (isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder),
[isLoading, isLoadingSuggestions, placeholder]
);

const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [
isLoading,
isLoadingSuggestions,
]);

return (
<EuiComboBox
placeholder={isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder}
isLoading={isLoading || isLoadingSuggestions}
placeholder={inputPlaceholder}
isLoading={isLoadingState}
isClearable={isClearable}
isDisabled={isDisabled}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
onSearchChange={onSearchChange}
onCreateOption={onCreateOption}
isInvalid={isRequired ? touched && (selectedValue.length === 0 || !isValid) : !isValid}
onFocus={() => setIsTouched(true)}
isInvalid={!isValid}
onFocus={setIsTouchedValue}
delimiter=", "
data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"
fullWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
existsOperator,
doesNotExistOperator,
} from './operators';
import { getOperators, validateParams, getGenericComboBoxProps } from './helpers';
import { getOperators, paramIsValid, getGenericComboBoxProps } from './helpers';

describe('helpers', () => {
describe('#getOperators', () => {
Expand Down Expand Up @@ -53,27 +53,67 @@ describe('helpers', () => {
});
});

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

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

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

expect(isValid).toBeFalsy();
expect(isValid).toBeTruthy();
});

test('returns true if value is undefined and "isRequired" is false', () => {
const isValid = paramIsValid(undefined, getField('@timestamp'), false, false);

expect(isValid).toBeTruthy();
});

test('returns false if value is empty string when "isRequired" is true and "touched" is false', () => {
const isValid = paramIsValid('', getField('@timestamp'), true, false);

expect(isValid).toBeTruthy();
});

test('returns true if value is empty string and "isRequired" is false', () => {
const isValid = paramIsValid('', getField('@timestamp'), false, false);

expect(isValid).toBeTruthy();
});

test('returns true if type is "date" and value is valid', () => {
const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp'));
test('returns true if type is "date" and value is valid and "isRequired" is false', () => {
const isValid = paramIsValid(
'1994-11-05T08:15:30-05:00',
getField('@timestamp'),
false,
false
);

expect(isValid).toBeTruthy();
});

test('returns false if type is "date" and value is not valid', () => {
const isValid = validateParams('1593478826', getField('@timestamp'));
test('returns true if type is "date" and value is valid and "isRequired" is true', () => {
const isValid = paramIsValid(
'1994-11-05T08:15:30-05:00',
getField('@timestamp'),
true,
false
);

expect(isValid).toBeTruthy();
});

test('returns false if type is "date" and value is not valid and "isRequired" is false', () => {
const isValid = paramIsValid('1593478826', getField('@timestamp'), false, false);

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

test('returns false if type is "date" and value is not valid and "isRequired" is true', () => {
const isValid = paramIsValid('1593478826', getField('@timestamp'), true, true);

expect(isValid).toBeFalsy();
});
Expand Down
Loading

0 comments on commit f7a1679

Please sign in to comment.