[Security Solutions][Detection Engine] Removes dead duplicated code and marks other duplicated code (#105374)

## Summary

* Removes dead duplicated code from `security_solution` and  `lists`
* Adds notes and TODO's where we still have duplicated logic
* Adds notes where I saw that the original deviated from the copy from modifications in one file but not the other.
* DOES NOT fix the bugs existing in one copy but not the other. That should be done when the copied chunks are collapsed into a package. Instead see this issue where I marked those areas: https://github.com/elastic/kibana/issues/105378

See these two files where things have deviated from our duplications as an example:
[security_solution/public/common/components/autocomplete/field.tsx](https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx
)
[lists/public/exceptions/components/autocomplete/field.tsx](https://github.com/elastic/kibana/blob/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx)

Ref PR where fixes are applied to one of the files but not the other (could be other PR's in addition to this one):
https://github.com/elastic/kibana/pull/87004

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2021-07-13 16:23:58 -06:00 committed by GitHub
parent ef06cf7ec0
commit bdf1069e56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 63 additions and 1439 deletions

View file

@ -11,8 +11,7 @@ import {
ListOperatorEnum as OperatorEnum,
ListOperatorTypeEnum as OperatorTypeEnum,
} from '@kbn/securitysolution-io-ts-list-types';
import { OperatorOption } from './types';
import { OperatorOption } from '../types';
export const isOperator: OperatorOption = {
message: i18n.translate('lists.exceptions.isOperatorLabel', {

View file

@ -1,19 +0,0 @@
/*
* 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 type {
ListOperatorEnum as OperatorEnum,
ListOperatorTypeEnum as OperatorTypeEnum,
} from '@kbn/securitysolution-io-ts-list-types';
export interface OperatorOption {
message: string;
value: string;
operator: OperatorEnum;
type: OperatorTypeEnum;
}

View file

@ -43,7 +43,6 @@ import {
isOneOfOperator,
isOperator,
} from '../autocomplete_operators';
import { OperatorOption } from '../autocomplete_operators/types';
import {
BuilderEntry,
@ -52,6 +51,7 @@ import {
EmptyNestedEntry,
ExceptionsBuilderExceptionItem,
FormattedBuilderEntry,
OperatorOption,
} from '../types';
export const isEntryNested = (item: BuilderEntry): item is EntryNested => {

View file

@ -23,7 +23,12 @@ import {
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
} from '@kbn/securitysolution-list-constants';
import type { OperatorOption } from '../autocomplete_operators/types';
export interface OperatorOption {
message: string;
value: string;
operator: OperatorEnum;
type: OperatorTypeEnum;
}
/**
* @deprecated Use the one from core once it is in its own package which will be from:

View file

@ -28,6 +28,13 @@ interface OperatorProps {
selectedField: IFieldType | undefined;
}
/**
* There is a copy within:
* x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
* NOTE: This has deviated from the copy and will have to be reconciled.
*/
export const FieldComponent: React.FC<OperatorProps> = ({
fieldInputWidth,
fieldTypeFilter = [],

View file

@ -47,6 +47,11 @@ interface AutocompleteFieldMatchProps {
onError?: (arg: boolean) => void;
}
/**
* There is a copy of this within:
* x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*/
export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchProps> = ({
placeholder,
rowLabel,

View file

@ -10,6 +10,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui';
import type { ListSchema, Type } from '@kbn/securitysolution-io-ts-list-types';
import {
EXCEPTION_OPERATORS,
OperatorOption,
doesNotExistOperator,
existsOperator,
isNotOperator,
@ -18,7 +19,7 @@ import {
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { GetGenericComboBoxPropsReturn, OperatorOption } from './types';
import { GetGenericComboBoxPropsReturn } from './types';
import * as i18n from './translations';
/**
@ -72,6 +73,10 @@ export const checkEmptyValue = (
/**
* Very basic validation for values
* There is a copy within:
* x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*
* @param param the value being checked
* @param field the selected field
@ -109,7 +114,10 @@ export const paramIsValid = (
/**
* Determines the options, selected values and option labels for EUI combo box
* There is a copy within:
* x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
* @param options options user can select from
* @param selectedOptions user selection if any
* @param getLabel helper function to know which property to use for labels

View file

@ -33,7 +33,10 @@ export interface UseFieldValueAutocompleteProps {
}
/**
* Hook for using the field value autocomplete service
* There is a copy within:
* x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*/
export const useFieldValueAutocomplete = ({
selectedField,

View file

@ -7,11 +7,12 @@
import React, { useCallback, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { OperatorOption } from '@kbn/securitysolution-list-utils';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { getGenericComboBoxProps, getOperators } from './helpers';
import { GetGenericComboBoxPropsReturn, OperatorOption } from './types';
import { GetGenericComboBoxPropsReturn } from './types';
const AS_PLAIN_TEXT = { asPlainText: true };

View file

@ -6,20 +6,9 @@
*/
import { EuiComboBoxOptionOption } from '@elastic/eui';
import type {
ListOperatorEnum as OperatorEnum,
ListOperatorTypeEnum as OperatorTypeEnum,
} from '@kbn/securitysolution-io-ts-list-types';
export interface GetGenericComboBoxPropsReturn {
comboOptions: EuiComboBoxOptionOption[];
labels: string[];
selectedComboOptions: EuiComboBoxOptionOption[];
}
export interface OperatorOption {
message: string;
value: string;
operator: OperatorEnum;
type: OperatorTypeEnum;
}

View file

@ -18,6 +18,7 @@ import {
BuilderEntry,
EXCEPTION_OPERATORS_ONLY_LISTS,
FormattedBuilderEntry,
OperatorOption,
getEntryOnFieldChange,
getEntryOnListChange,
getEntryOnMatchAnyChange,
@ -32,7 +33,6 @@ import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data
import { HttpStart } from '../../../../../../../src/core/public';
import { FieldComponent } from '../autocomplete/field';
import { OperatorComponent } from '../autocomplete/operator';
import { OperatorOption } from '../autocomplete/types';
import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_exists';
import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match';
import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any';

View file

@ -24,6 +24,7 @@ import {
EmptyEntry,
ExceptionsBuilderExceptionItem,
FormattedBuilderEntry,
OperatorOption,
doesNotExistOperator,
existsOperator,
filterExceptionItems,
@ -64,7 +65,6 @@ import { getEntryNestedMock } from '../../../../common/schemas/types/entry_neste
import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock';
import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock';
import { getListResponseMock } from '../../../../common/schemas/response/list_schema.mock';
import { OperatorOption } from '../autocomplete/types';
import { getEntryListMock } from '../../../../common/schemas/types/entry_list.mock';
// TODO: ALL THESE TESTS SHOULD BE MOVED TO @kbn/securitysolution-list-utils for its helper. The only reason why they're here is due to missing other packages we hae to create or missing things from kbn packages such as mocks from kibana core

View file

@ -25,6 +25,13 @@ interface OperatorProps {
onChange: (a: IFieldType[]) => void;
}
/**
* There is a copy within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
* NOTE: This has deviated from the copy and will have to be reconciled.
*/
export const FieldComponent: React.FC<OperatorProps> = ({
placeholder,
selectedField,

View file

@ -1,23 +0,0 @@
/*
* 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 { mount } from 'enzyme';
import { AutocompleteFieldExistsComponent } from './field_value_exists';
describe('AutocompleteFieldExistsComponent', () => {
test('it renders field disabled', () => {
const wrapper = mount(<AutocompleteFieldExistsComponent placeholder="Placeholder text" />);
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox existsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
});
});

View file

@ -1,33 +0,0 @@
/*
* 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 { EuiFormRow, EuiComboBox } from '@elastic/eui';
interface AutocompleteFieldExistsProps {
placeholder: string;
rowLabel?: string;
}
export const AutocompleteFieldExistsComponent: React.FC<AutocompleteFieldExistsProps> = ({
placeholder,
rowLabel,
}): JSX.Element => (
<EuiFormRow label={rowLabel} fullWidth>
<EuiComboBox
placeholder={placeholder}
options={[]}
selectedOptions={[]}
onChange={undefined}
isDisabled
data-test-subj="valuesAutocompleteComboBox existsComboxBox"
fullWidth
/>
</EuiFormRow>
);
AutocompleteFieldExistsComponent.displayName = 'AutocompleteFieldExists';

View file

@ -1,214 +0,0 @@
/*
* 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 { mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock';
import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock';
import { DATE_NOW, VERSION, IMMUTABLE } from '../../../../../lists/common/constants.mock';
import { AutocompleteFieldListsComponent } from './field_value_lists';
jest.mock('../../../common/lib/kibana');
const mockStart = jest.fn();
const mockKeywordList: ListSchema = {
...getListResponseMock(),
id: 'keyword_list',
type: 'keyword',
name: 'keyword list',
};
const mockResult = { ...getFoundListSchemaMock() };
mockResult.data = [...mockResult.data, mockKeywordList];
jest.mock('@kbn/securitysolution-list-hooks', () => {
const originalModule = jest.requireActual('@kbn/securitysolution-list-hooks');
return {
...originalModule,
useFindLists: () => ({
loading: false,
start: mockStart.mockReturnValue(mockResult),
result: mockResult,
error: undefined,
}),
};
});
describe('AutocompleteFieldListsComponent', () => {
test('it renders disabled if "isDisabled" is true', async () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
isLoading={false}
isClearable={true}
isDisabled
onChange={jest.fn()}
/>
);
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
});
test('it renders loading if "isLoading" is true', async () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
isLoading
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
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();
});
test('it allows user to clear values if "isClearable" is true', async () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
expect(
wrapper
.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', () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
expect(
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'keyword list' }]);
});
test('it correctly displays lists that match the selected "ip" field esType', () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');
expect(
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'some name' }]);
});
test('it correctly displays selected list', async () => {
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] EuiComboBoxPill`)
.at(0)
.text()
).toEqual('some name');
});
test('it invokes "onChange" when option selected', async () => {
const mockOnChange = jest.fn();
const wrapper = mount(
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={mockOnChange}
/>
);
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: 'some name' }]);
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,
});
});
});
});

View file

@ -1,121 +0,0 @@
/*
* 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, { useState, useEffect, useCallback, useMemo } from 'react';
import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { useFindLists } from '@kbn/securitysolution-list-hooks';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { useKibana } from '../../../common/lib/kibana';
import { filterFieldToList, getGenericComboBoxProps } from './helpers';
import * as i18n from './translations';
interface AutocompleteFieldListsProps {
placeholder: string;
selectedField: IFieldType | undefined;
selectedValue: string | undefined;
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isRequired?: boolean;
rowLabel?: string;
onChange: (arg: ListSchema) => void;
}
export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsProps> = ({
placeholder,
rowLabel,
selectedField,
selectedValue,
isLoading = false,
isDisabled = false,
isClearable = false,
isRequired = false,
onChange,
}): JSX.Element => {
const [error, setError] = useState<string | undefined>(undefined);
const { http } = useKibana().services;
const [lists, setLists] = useState<ListSchema[]>([]);
const { loading, result, start } = useFindLists();
const getLabel = useCallback(({ name }) => name, []);
const optionsMemo = useMemo(() => filterFieldToList(lists, selectedField), [
lists,
selectedField,
]);
const selectedOptionsMemo = useMemo(() => {
if (selectedValue != null) {
const list = lists.filter(({ id }) => id === selectedValue);
return list ?? [];
} else {
return [];
}
}, [selectedValue, lists]);
const { comboOptions, labels, selectedComboOptions } = useMemo(
() =>
getGenericComboBoxProps<ListSchema>({
options: optionsMemo,
selectedOptions: selectedOptionsMemo,
getLabel,
}),
[optionsMemo, selectedOptionsMemo, getLabel]
);
const handleValuesChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]) => {
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
onChange(newValue ?? '');
},
[labels, optionsMemo, onChange]
);
const setIsTouchedValue = useCallback((): void => {
setError(selectedValue == null ? i18n.FIELD_REQUIRED_ERR : undefined);
}, [selectedValue]);
useEffect(() => {
if (result != null) {
setLists(result.data);
}
}, [result]);
useEffect(() => {
if (selectedField != null) {
start({
http,
pageIndex: 1,
pageSize: 500,
});
}
}, [selectedField, start, http]);
const isLoadingState = useMemo((): boolean => isLoading || loading, [isLoading, loading]);
return (
<EuiFormRow label={rowLabel} error={error} isInvalid={error != null} fullWidth>
<EuiComboBox
placeholder={placeholder}
isDisabled={isDisabled}
isLoading={isLoadingState}
isClearable={isClearable}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
isInvalid={error != null}
onBlur={setIsTouchedValue}
singleSelection={{ asPlainText: true }}
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteComboBox listsComboxBox"
fullWidth
async
/>
</EuiFormRow>
);
};
AutocompleteFieldListsComponent.displayName = 'AutocompleteFieldList';

View file

@ -38,6 +38,11 @@ interface AutocompleteFieldMatchProps {
onError?: (arg: boolean) => void;
}
/**
* There is a copy of this within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*/
export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchProps> = ({
placeholder,
rowLabel,

View file

@ -1,238 +0,0 @@
/*
* 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 { mount, ReactWrapper } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { act } from '@testing-library/react';
import {
fields,
getField,
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { AutocompleteFieldMatchAnyComponent } from './field_value_match_any';
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
jest.mock('./hooks/use_field_value_autocomplete');
describe('AutocompleteFieldMatchAnyComponent', () => {
let wrapper: ReactWrapper;
const getValueSuggestionsMock = jest
.fn()
.mockResolvedValue([false, true, ['value 3', 'value 4'], jest.fn()]);
beforeEach(() => {
(useFieldValueAutocomplete as jest.Mock).mockReturnValue([
false,
true,
['value 1', 'value 2'],
getValueSuggestionsMock,
]);
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('it renders disabled if "isDisabled" is true', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue={['126.45.211.34']}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={false}
isDisabled={true}
onChange={jest.fn()}
onError={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] input`).prop('disabled')
).toBeTruthy();
});
test('it renders loading if "isLoading" is true', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue={[]}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={true}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
onError={jest.fn()}
/>
);
wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] button`).at(0).simulate('click');
expect(
wrapper
.find(`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteMatchAny-optionsList"]`)
.prop('isLoading')
).toBeTruthy();
});
test('it allows user to clear values if "isClearable" is true', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue={['126.45.211.34']}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={true}
isDisabled={false}
onChange={jest.fn()}
onError={jest.fn()}
/>
);
expect(
wrapper
.find(`[data-test-subj="comboBoxInput"]`)
.hasClass('euiComboBox__inputWrap-isClearable')
).toBeTruthy();
});
test('it correctly displays selected value', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue={['126.45.211.34']}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
onError={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] EuiComboBoxPill`).at(0).text()
).toEqual('126.45.211.34');
});
test('it invokes "onChange" when new value created', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue={[]}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={mockOnChange}
onError={jest.fn()}
/>
);
((wrapper.find(EuiComboBox).props() as unknown) as {
onCreateOption: (a: string) => void;
}).onCreateOption('126.45.211.34');
expect(mockOnChange).toHaveBeenCalledWith(['126.45.211.34']);
});
test('it invokes "onChange" when new value selected', async () => {
const mockOnChange = jest.fn();
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue={[]}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={mockOnChange}
onError={jest.fn()}
/>
);
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: 'value 1' }]);
expect(mockOnChange).toHaveBeenCalledWith(['value 1']);
});
test('it refreshes autocomplete with search query when new value searched', () => {
wrapper = mount(
<AutocompleteFieldMatchAnyComponent
rowLabel={'Row Label'}
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
selectedValue={[]}
indexPattern={{
id: '1234',
title: 'logstash-*',
fields,
}}
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
);
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
onSearchChange: (a: string) => void;
}).onSearchChange('value 1');
});
expect(useFieldValueAutocomplete).toHaveBeenCalledWith({
selectedField: getField('machine.os.raw'),
operatorType: 'match_any',
query: 'value 1',
fieldValue: [],
indexPattern: {
id: '1234',
title: 'logstash-*',
fields,
},
});
});
});

View file

@ -1,221 +0,0 @@
/*
* 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, { useState, useCallback, useMemo } from 'react';
import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { uniq } from 'lodash';
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete';
import { getGenericComboBoxProps, paramIsValid } from './helpers';
import { GetGenericComboBoxPropsReturn } from './types';
import * as i18n from './translations';
interface AutocompleteFieldMatchAnyProps {
placeholder: string;
selectedField: IFieldType | undefined;
selectedValue: string[];
indexPattern: IIndexPattern | undefined;
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isRequired?: boolean;
rowLabel?: string;
onChange: (arg: string[]) => void;
onError?: (arg: boolean) => void;
}
export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatchAnyProps> = ({
placeholder,
rowLabel,
selectedField,
selectedValue,
indexPattern,
isLoading,
isDisabled = false,
isClearable = false,
isRequired = false,
onChange,
onError,
}): JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const [touched, setIsTouched] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [isLoadingSuggestions, isSuggestingValues, suggestions] = useFieldValueAutocomplete({
selectedField,
operatorType: OperatorTypeEnum.MATCH_ANY,
fieldValue: selectedValue,
query: searchQuery,
indexPattern,
});
const getLabel = useCallback((option: string): string => option, []);
const optionsMemo = useMemo(
(): string[] => (selectedValue ? uniq([...selectedValue, ...suggestions]) : suggestions),
[suggestions, selectedValue]
);
const { comboOptions, labels, selectedComboOptions } = useMemo(
(): GetGenericComboBoxPropsReturn =>
getGenericComboBoxProps<string>({
options: optionsMemo,
selectedOptions: selectedValue,
getLabel,
}),
[optionsMemo, selectedValue, getLabel]
);
const handleError = useCallback(
(err: string | undefined): void => {
setError((existingErr): string | undefined => {
const oldErr = existingErr != null;
const newErr = err != null;
if (oldErr !== newErr && onError != null) {
onError(newErr);
}
return err;
});
},
[setError, onError]
);
const handleValuesChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]): void => {
const newValues: string[] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
handleError(undefined);
onChange(newValues);
},
[handleError, labels, onChange, optionsMemo]
);
const handleSearchChange = useCallback(
(searchVal: string) => {
if (searchVal === '') {
handleError(undefined);
}
if (searchVal !== '' && selectedField != null) {
const err = paramIsValid(searchVal, selectedField, isRequired, touched);
handleError(err);
setSearchQuery(searchVal);
}
},
[handleError, isRequired, selectedField, touched]
);
const handleCreateOption = useCallback(
(option: string): boolean | void => {
const err = paramIsValid(option, selectedField, isRequired, touched);
handleError(err);
if (err != null) {
// Explicitly reject the user's input
return false;
} else {
onChange([...(selectedValue || []), option]);
}
},
[handleError, isRequired, onChange, selectedField, selectedValue, touched]
);
const setIsTouchedValue = useCallback((): void => {
handleError(selectedComboOptions.length === 0 ? i18n.FIELD_REQUIRED_ERR : undefined);
setIsTouched(true);
}, [setIsTouched, handleError, selectedComboOptions]);
const inputPlaceholder = useMemo(
(): string => (isLoading || isLoadingSuggestions ? i18n.LOADING : placeholder),
[isLoading, isLoadingSuggestions, placeholder]
);
const isLoadingState = useMemo((): boolean => isLoading || isLoadingSuggestions, [
isLoading,
isLoadingSuggestions,
]);
const defaultInput = useMemo((): JSX.Element => {
return (
<EuiFormRow
label={rowLabel}
error={error}
isInvalid={selectedField != null && error != null}
fullWidth
>
<EuiComboBox
placeholder={inputPlaceholder}
isLoading={isLoadingState}
isClearable={isClearable}
isDisabled={isDisabled}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
onSearchChange={handleSearchChange}
onCreateOption={handleCreateOption}
isInvalid={selectedField != null && error != null}
onBlur={setIsTouchedValue}
delimiter=", "
data-test-subj="valuesAutocompleteMatchAny"
fullWidth
async
/>
</EuiFormRow>
);
}, [
comboOptions,
error,
handleCreateOption,
handleSearchChange,
handleValuesChange,
inputPlaceholder,
isClearable,
isDisabled,
isLoadingState,
rowLabel,
selectedComboOptions,
selectedField,
setIsTouchedValue,
]);
if (!isSuggestingValues && selectedField != null) {
switch (selectedField.type) {
case 'number':
return (
<EuiFormRow
label={rowLabel}
error={error}
isInvalid={selectedField != null && error != null}
fullWidth
>
<EuiComboBox
noSuggestions
placeholder={inputPlaceholder}
isLoading={isLoadingState}
isClearable={isClearable}
isDisabled={isDisabled}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
onSearchChange={handleSearchChange}
onCreateOption={handleCreateOption}
isInvalid={selectedField != null && error != null}
onFocus={setIsTouchedValue}
delimiter=", "
data-test-subj="valuesAutocompleteMatchAnyNumber"
fullWidth
/>
</EuiFormRow>
);
default:
return defaultInput;
}
}
return defaultInput;
};
AutocompleteFieldMatchAnyComponent.displayName = 'AutocompleteFieldMatchAny';

View file

@ -8,65 +8,13 @@
import moment from 'moment';
import '../../../common/mock/match_media';
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import * as i18n from './translations';
import {
EXCEPTION_OPERATORS,
isOperator,
isNotOperator,
existsOperator,
doesNotExistOperator,
} from '@kbn/securitysolution-list-utils';
import {
getOperators,
checkEmptyValue,
paramIsValid,
getGenericComboBoxProps,
typeMatch,
filterFieldToList,
} from './helpers';
import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock';
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { checkEmptyValue, paramIsValid, getGenericComboBoxProps } from './helpers';
describe('helpers', () => {
// @ts-ignore
moment.suppressDeprecationWarnings = true;
describe('#getOperators', () => {
test('it returns "isOperator" if passed in field is "undefined"', () => {
const operator = getOperators(undefined);
expect(operator).toEqual([isOperator]);
});
test('it returns expected operators when field type is "boolean"', () => {
const operator = getOperators(getField('ssl'));
expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]);
});
test('it returns "isOperator" when field type is "nested"', () => {
const operator = getOperators({
name: 'nestedField',
type: 'nested',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
subType: { nested: { path: 'nestedField' } },
});
expect(operator).toEqual([isOperator]);
});
test('it returns all operator types when field type is not null, boolean, or nested', () => {
const operator = getOperators(getField('machine.os.raw'));
expect(operator).toEqual(EXCEPTION_OPERATORS);
});
});
describe('#checkEmptyValue', () => {
test('returns no errors if no field has been selected', () => {
@ -272,117 +220,4 @@ describe('helpers', () => {
});
});
});
describe('#typeMatch', () => {
test('ip -> ip is true', () => {
expect(typeMatch('ip', 'ip')).toEqual(true);
});
test('keyword -> keyword is true', () => {
expect(typeMatch('keyword', 'keyword')).toEqual(true);
});
test('text -> text is true', () => {
expect(typeMatch('text', 'text')).toEqual(true);
});
test('ip_range -> ip is true', () => {
expect(typeMatch('ip_range', 'ip')).toEqual(true);
});
test('date_range -> date is true', () => {
expect(typeMatch('date_range', 'date')).toEqual(true);
});
test('double_range -> double is true', () => {
expect(typeMatch('double_range', 'double')).toEqual(true);
});
test('float_range -> float is true', () => {
expect(typeMatch('float_range', 'float')).toEqual(true);
});
test('integer_range -> integer is true', () => {
expect(typeMatch('integer_range', 'integer')).toEqual(true);
});
test('long_range -> long is true', () => {
expect(typeMatch('long_range', 'long')).toEqual(true);
});
test('ip -> date is false', () => {
expect(typeMatch('ip', 'date')).toEqual(false);
});
test('long -> float is false', () => {
expect(typeMatch('long', 'float')).toEqual(false);
});
test('integer -> long is false', () => {
expect(typeMatch('integer', 'long')).toEqual(false);
});
});
describe('#filterFieldToList', () => {
test('it returns empty array if given a undefined for field', () => {
const filter = filterFieldToList([], undefined);
expect(filter).toEqual([]);
});
test('it returns empty array if filed does not contain esTypes', () => {
const field: IFieldType = { name: 'some-name', type: 'some-type' };
const filter = filterFieldToList([], field);
expect(filter).toEqual([]);
});
test('it returns single filtered list of ip_range -> ip', () => {
const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] };
const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
expect(filter).toEqual(expected);
});
test('it returns single filtered list of ip -> ip', () => {
const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] };
const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
expect(filter).toEqual(expected);
});
test('it returns single filtered list of keyword -> keyword', () => {
const field: IFieldType = { name: 'some-name', type: 'keyword', esTypes: ['keyword'] };
const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
expect(filter).toEqual(expected);
});
test('it returns single filtered list of text -> text', () => {
const field: IFieldType = { name: 'some-name', type: 'text', esTypes: ['text'] };
const listItem: ListSchema = { ...getListResponseMock(), type: 'text' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
expect(filter).toEqual(expected);
});
test('it returns 2 filtered lists of ip_range -> ip', () => {
const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] };
const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const filter = filterFieldToList([listItem1, listItem2], field);
const expected: ListSchema[] = [listItem1, listItem2];
expect(filter).toEqual(expected);
});
test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => {
const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] };
const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' };
const filter = filterFieldToList([listItem1, listItem2], field);
const expected: ListSchema[] = [listItem1];
expect(filter).toEqual(expected);
});
});
});

View file

@ -8,46 +8,17 @@
import dateMath from '@elastic/datemath';
import { EuiComboBoxOptionOption } from '@elastic/eui';
import type { Type, ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import {
EXCEPTION_OPERATORS,
isOperator,
isNotOperator,
existsOperator,
doesNotExistOperator,
} from '@kbn/securitysolution-list-utils';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { GetGenericComboBoxPropsReturn, OperatorOption } from './types';
import { GetGenericComboBoxPropsReturn } from './types';
import * as i18n from './translations';
/**
* Returns the appropriate operators given a field type
*
* @param field IFieldType selected field
*
*/
export const getOperators = (field: IFieldType | undefined): OperatorOption[] => {
if (field == null) {
return [isOperator];
} else if (field.type === 'boolean') {
return [isOperator, isNotOperator, existsOperator, doesNotExistOperator];
} else if (field.type === 'nested') {
return [isOperator];
} else {
return EXCEPTION_OPERATORS;
}
};
/**
* Determines if empty value is ok
* There is a copy within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
*
* @param param the value being checked
* @param field the selected field
* @param isRequired whether or not an empty value is allowed
* @param touched has field been touched by user
* @returns undefined if valid, string with error message if invalid,
* null if no checks matched
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*/
export const checkEmptyValue = (
param: string | undefined,
@ -72,7 +43,10 @@ export const checkEmptyValue = (
/**
* Very basic validation for values
* There is a copy within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
* @param param the value being checked
* @param field the selected field
* @param isRequired whether or not an empty value is allowed
@ -109,7 +83,10 @@ export const paramIsValid = (
/**
* Determines the options, selected values and option labels for EUI combo box
* There is a copy within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
* @param options options user can select from
* @param selectedOptions user selection if any
* @param getLabel helper function to know which property to use for labels
@ -140,36 +117,3 @@ export function getGenericComboBoxProps<T>({
selectedComboOptions: newSelectedComboOptions,
};
}
/**
* Given an array of lists and optionally a field this will return all
* the lists that match against the field based on the types from the field
* @param lists The lists to match against the field
* @param field The field to check against the list to see if they are compatible
*/
export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => {
if (field != null) {
const { esTypes = [] } = field;
return lists.filter(({ type }) => esTypes.some((esType) => typeMatch(type, esType)));
} else {
return [];
}
};
/**
* Given an input list type and a string based ES type this will match
* if they're exact or if they are compatible with a range
* @param type The type to match against the esType
* @param esType The ES type to match with
*/
export const typeMatch = (type: Type, esType: string): boolean => {
return (
type === esType ||
(type === 'ip_range' && esType === 'ip') ||
(type === 'date_range' && esType === 'date') ||
(type === 'double_range' && esType === 'double') ||
(type === 'float_range' && esType === 'float') ||
(type === 'integer_range' && esType === 'integer') ||
(type === 'long_range' && esType === 'long')
);
};

View file

@ -30,9 +30,13 @@ export interface UseFieldValueAutocompleteProps {
query: string;
indexPattern: IIndexPattern | undefined;
}
/**
* Hook for using the field value autocomplete service
* There is a copy within:
* x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks.ts
*
* TODO: This should be in its own packaged and not copied, https://github.com/elastic/kibana/issues/105378
*/
export const useFieldValueAutocomplete = ({
selectedField,

View file

@ -1,225 +0,0 @@
/*
* 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 { mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { OperatorComponent } from './operator';
import { isOperator, isNotOperator } from '@kbn/securitysolution-list-utils';
describe('OperatorComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={true}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] input`).prop('disabled')
).toBeTruthy();
});
test('it renders loading if "isLoading" is true', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={true}
isClearable={false}
onChange={jest.fn()}
/>
);
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] button`).at(0).simulate('click');
expect(
wrapper
.find(`EuiComboBoxOptionsList[data-test-subj="operatorAutocompleteComboBox-optionsList"]`)
.prop('isLoading')
).toBeTruthy();
});
test('it allows user to clear values if "isClearable" is true', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={true}
onChange={jest.fn()}
/>
);
expect(wrapper.find(`button[data-test-subj="comboBoxClearButton"]`).exists()).toBeTruthy();
});
test('it displays "operatorOptions" if param is passed in with items', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
operatorOptions={[isNotOperator]}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options')
).toEqual([{ label: 'is not' }]);
});
test('it does not display "operatorOptions" if param is passed in with no items', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
operatorOptions={[]}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options')
).toEqual([
{
label: 'is',
},
{
label: 'is not',
},
{
label: 'is one of',
},
{
label: 'is not one of',
},
{
label: 'exists',
},
{
label: 'does not exist',
},
{
label: 'is in list',
},
{
label: 'is not in list',
},
]);
});
test('it correctly displays selected operator', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] EuiComboBoxPill`).at(0).text()
).toEqual('is');
});
test('it only displays subset of operators if field type is nested', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={{
name: 'nestedField',
type: 'nested',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
subType: { nested: { path: 'nestedField' } },
}}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options')
).toEqual([{ label: 'is' }]);
});
test('it only displays subset of operators if field type is boolean', () => {
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('ssl')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={jest.fn()}
/>
);
expect(
wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options')
).toEqual([
{ label: 'is' },
{ label: 'is not' },
{ label: 'exists' },
{ label: 'does not exist' },
]);
});
test('it invokes "onChange" when option selected', () => {
const mockOnChange = jest.fn();
const wrapper = mount(
<OperatorComponent
placeholder="Placeholder text"
selectedField={getField('machine.os.raw')}
operator={isOperator}
isDisabled={false}
isLoading={false}
isClearable={false}
onChange={mockOnChange}
/>
);
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: 'is not' }]);
expect(mockOnChange).toHaveBeenCalledWith([
{ message: 'is not', operator: 'excluded', type: 'match', value: 'is_not' },
]);
});
});

View file

@ -1,82 +0,0 @@
/*
* 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, { useCallback, useMemo } from 'react';
import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { IFieldType } from '../../../../../../../src/plugins/data/common';
import { getOperators, getGenericComboBoxProps } from './helpers';
import { GetGenericComboBoxPropsReturn, OperatorOption } from './types';
interface OperatorState {
placeholder: string;
selectedField: IFieldType | undefined;
operator: OperatorOption;
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
operatorInputWidth?: number;
operatorOptions?: OperatorOption[];
onChange: (arg: OperatorOption[]) => void;
}
export const OperatorComponent: React.FC<OperatorState> = ({
placeholder,
selectedField,
operator,
isLoading = false,
isDisabled = false,
isClearable = false,
operatorOptions,
operatorInputWidth = 150,
onChange,
}): JSX.Element => {
const getLabel = useCallback(({ message }): string => message, []);
const optionsMemo = useMemo(
(): OperatorOption[] =>
operatorOptions != null && operatorOptions.length > 0
? operatorOptions
: getOperators(selectedField),
[operatorOptions, selectedField]
);
const selectedOptionsMemo = useMemo((): OperatorOption[] => (operator ? [operator] : []), [
operator,
]);
const { comboOptions, labels, selectedComboOptions } = useMemo(
(): GetGenericComboBoxPropsReturn =>
getGenericComboBoxProps<OperatorOption>({
options: optionsMemo,
selectedOptions: selectedOptionsMemo,
getLabel,
}),
[optionsMemo, selectedOptionsMemo, getLabel]
);
const handleValuesChange = (newOptions: EuiComboBoxOptionOption[]): void => {
const newValues: OperatorOption[] = newOptions.map(
({ label }) => optionsMemo[labels.indexOf(label)]
);
onChange(newValues);
};
return (
<EuiComboBox
placeholder={placeholder}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
isLoading={isLoading}
isDisabled={isDisabled}
isClearable={isClearable}
singleSelection={{ asPlainText: true }}
data-test-subj="operatorAutocompleteComboBox"
style={{ width: `${operatorInputWidth}px` }}
/>
);
};
OperatorComponent.displayName = 'Operator';

View file

@ -7,20 +7,8 @@
import { EuiComboBoxOptionOption } from '@elastic/eui';
import type {
ListOperatorEnum as OperatorEnum,
ListOperatorTypeEnum as OperatorTypeEnum,
} from '@kbn/securitysolution-io-ts-list-types';
export interface GetGenericComboBoxPropsReturn {
comboOptions: EuiComboBoxOptionOption[];
labels: string[];
selectedComboOptions: EuiComboBoxOptionOption[];
}
export interface OperatorOption {
message: string;
value: string;
operator: OperatorEnum;
type: OperatorTypeEnum;
}