[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:
parent
ef06cf7ec0
commit
bdf1069e56
|
@ -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', {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = [],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue