Skip to content

Commit

Permalink
[Security Solution] Value list exceptions (#133254)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee authored Sep 19, 2022
1 parent 672bdd2 commit 51699fa
Show file tree
Hide file tree
Showing 113 changed files with 4,666 additions and 2,677 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getField } from '../fields/index.mock';
import { AutocompleteFieldListsComponent } from '.';
import {
getListResponseMock,
getFoundListSchemaMock,
getFoundListsBySizeSchemaMock,
DATE_NOW,
IMMUTABLE,
VERSION,
Expand All @@ -34,14 +34,15 @@ const mockKeywordList: ListSchema = {
name: 'keyword list',
type: 'keyword',
};
const mockResult = { ...getFoundListSchemaMock() };
mockResult.data = [...mockResult.data, mockKeywordList];
const mockResult = { ...getFoundListsBySizeSchemaMock() };
mockResult.smallLists = [...mockResult.smallLists, mockKeywordList];
mockResult.largeLists = [];
jest.mock('@kbn/securitysolution-list-hooks', () => {
const originalModule = jest.requireActual('@kbn/securitysolution-list-hooks');

return {
...originalModule,
useFindLists: () => ({
useFindListsBySize: () => ({
error: undefined,
loading: false,
result: mockResult,
Expand Down Expand Up @@ -116,7 +117,7 @@ describe('AutocompleteFieldListsComponent', () => {
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'some name' }]);
).toEqual([{ label: 'some name', disabled: false }]);
});

test('it correctly displays lists that match the selected "keyword" field esType', () => {
Expand All @@ -139,7 +140,7 @@ describe('AutocompleteFieldListsComponent', () => {
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'keyword list' }]);
).toEqual([{ label: 'keyword list', disabled: false }]);
});

test('it correctly displays lists that match the selected "ip" field esType', () => {
Expand All @@ -162,7 +163,7 @@ describe('AutocompleteFieldListsComponent', () => {
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'some name' }]);
).toEqual([{ label: 'some name', disabled: false }]);
});

test('it correctly displays selected list', async () => {
Expand Down Expand Up @@ -206,7 +207,7 @@ describe('AutocompleteFieldListsComponent', () => {
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([{ label: 'some name' }]);
).onChange([{ label: 'some name', disabled: false }]);

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } from '@elastic/eui';
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { useFindLists } from '@kbn/securitysolution-list-hooks';
import { useFindListsBySize } from '@kbn/securitysolution-list-hooks';
import { DataViewFieldBase } from '@kbn/es-query';

import { filterFieldToList } from '../filter_field_to_list';
Expand All @@ -33,6 +33,12 @@ interface AutocompleteFieldListsProps {
rowLabel?: string;
selectedField: DataViewFieldBase | undefined;
selectedValue: string | undefined;
allowLargeValueLists?: boolean;
}

export interface AutocompleteListsData {
smallLists: ListSchema[];
largeLists: ListSchema[];
}

export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsProps> = ({
Expand All @@ -45,37 +51,44 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
rowLabel,
selectedField,
selectedValue,
allowLargeValueLists = false,
}): JSX.Element => {
const [error, setError] = useState<string | undefined>(undefined);
const [lists, setLists] = useState<ListSchema[]>([]);
const { loading, result, start } = useFindLists();
const [listData, setListData] = useState<AutocompleteListsData>({
smallLists: [],
largeLists: [],
});
const { loading, result, start } = useFindListsBySize();
const getLabel = useCallback(({ name }) => name, []);

const optionsMemo = useMemo(
() => filterFieldToList(lists, selectedField),
[lists, selectedField]
() => filterFieldToList(listData, selectedField),
[listData, selectedField]
);
const selectedOptionsMemo = useMemo(() => {
if (selectedValue != null) {
const list = lists.filter(({ id }) => id === selectedValue);
const combinedLists = [...listData.smallLists, ...listData.largeLists];
const list = combinedLists.filter(({ id }) => id === selectedValue);
return list ?? [];
} else {
return [];
}
}, [selectedValue, lists]);
}, [selectedValue, listData]);
const { comboOptions, labels, selectedComboOptions } = useMemo(
() =>
getGenericComboBoxProps<ListSchema>({
getLabel,
options: optionsMemo,
options: [...optionsMemo.smallLists, ...optionsMemo.largeLists],
selectedOptions: selectedOptionsMemo,
disabledOptions: allowLargeValueLists ? undefined : optionsMemo.largeLists, // Disable large lists if the rule type doesn't allow it
}),
[optionsMemo, selectedOptionsMemo, getLabel]
[optionsMemo, selectedOptionsMemo, getLabel, allowLargeValueLists]
);

const handleValuesChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]) => {
const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]);
const combinedLists = [...optionsMemo.smallLists, ...optionsMemo.largeLists];
const [newValue] = newOptions.map(({ label }) => combinedLists[labels.indexOf(label)]);
onChange(newValue ?? '');
},
[labels, optionsMemo, onChange]
Expand All @@ -87,7 +100,7 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro

useEffect(() => {
if (result != null) {
setLists(result.data);
setListData(result);
}
}, [result]);

Expand All @@ -103,8 +116,27 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro

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

const helpText = useMemo(() => {
return (
!allowLargeValueLists && (
<EuiText size="xs">
{i18n.LISTS_TOOLTIP_INFO}{' '}
<EuiLink external target="_blank" href="https://www.elastic.co/">
{i18n.SEE_DOCUMENTATION}
</EuiLink>
</EuiText>
)
);
}, [allowLargeValueLists]);

return (
<EuiFormRow label={rowLabel} error={error} isInvalid={error != null} fullWidth>
<EuiFormRow
label={rowLabel}
error={error}
isInvalid={error != null}
helpText={helpText}
fullWidth
>
<EuiComboBox
async
data-test-subj="valuesAutocompleteComboBox listsComboxBox"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,83 +8,99 @@

import { filterFieldToList } from '.';

import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getListResponseMock } from '../list_schema/index.mock';
import { DataViewFieldBase } from '@kbn/es-query';
import { AutocompleteListsData } from '../field_value_lists';

const emptyListData: AutocompleteListsData = { smallLists: [], largeLists: [] };

describe('#filterFieldToList', () => {
test('it returns empty array if given a undefined for field', () => {
const filter = filterFieldToList([], undefined);
expect(filter).toEqual([]);
test('it returns empty list data object if given a undefined for field', () => {
const filter = filterFieldToList(emptyListData, undefined);
expect(filter).toEqual(emptyListData);
});

test('it returns empty array if filed does not contain esTypes', () => {
test('it returns empty list data object if filed does not contain esTypes', () => {
const field: DataViewFieldBase = {
name: 'some-name',
type: 'some-type',
};
const filter = filterFieldToList([], field);
expect(filter).toEqual([]);
const filter = filterFieldToList(emptyListData, field);
expect(filter).toEqual(emptyListData);
});

test('it returns single filtered list of ip_range -> ip', () => {
test('it returns filtered lists of ip_range -> ip', () => {
const field: DataViewFieldBase & { esTypes: string[] } = {
esTypes: ['ip'],
name: 'some-name',
type: 'ip',
};
const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'ip_range' }],
largeLists: [],
};
const filter = filterFieldToList(listData, field);
const expected = listData;
expect(filter).toEqual(expected);
});

test('it returns single filtered list of ip -> ip', () => {
test('it returns filtered lists of ip -> ip', () => {
const field: DataViewFieldBase & { esTypes: string[] } = {
esTypes: ['ip'],
name: 'some-name',
type: 'ip',
};
const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'ip' }],
largeLists: [],
};
const filter = filterFieldToList(listData, field);
const expected = listData;
expect(filter).toEqual(expected);
});

test('it returns single filtered list of keyword -> keyword', () => {
test('it returns filtered lists of keyword -> keyword', () => {
const field: DataViewFieldBase & { esTypes: string[] } = {
esTypes: ['keyword'],
name: 'some-name',
type: 'keyword',
};
const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'keyword' }],
largeLists: [],
};
const filter = filterFieldToList(listData, field);
const expected = listData;
expect(filter).toEqual(expected);
});

test('it returns single filtered list of text -> text', () => {
test('it returns filtered lists of text -> text', () => {
const field: DataViewFieldBase & { esTypes: string[] } = {
esTypes: ['text'],
name: 'some-name',
type: 'text',
};
const listItem: ListSchema = { ...getListResponseMock(), type: 'text' };
const filter = filterFieldToList([listItem], field);
const expected: ListSchema[] = [listItem];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'text' }],
largeLists: [],
};
const filter = filterFieldToList(listData, field);
const expected = listData;
expect(filter).toEqual(expected);
});

test('it returns 2 filtered lists of ip_range -> ip', () => {
test('it returns small and large filtered lists of ip_range -> ip', () => {
const field: DataViewFieldBase & { esTypes: string[] } = {
esTypes: ['ip'],
name: 'some-name',
type: '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];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'ip_range' }],
largeLists: [{ ...getListResponseMock(), type: 'ip_range' }],
};
const filter = filterFieldToList(listData, field);
const expected = listData;
expect(filter).toEqual(expected);
});

Expand All @@ -94,10 +110,15 @@ describe('#filterFieldToList', () => {
name: 'some-name',
type: 'ip',
};
const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' };
const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' };
const filter = filterFieldToList([listItem1, listItem2], field);
const expected: ListSchema[] = [listItem1];
const listData: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'ip_range' }],
largeLists: [{ ...getListResponseMock(), type: 'text' }],
};
const filter = filterFieldToList(listData, field);
const expected: AutocompleteListsData = {
smallLists: [{ ...getListResponseMock(), type: 'ip_range' }],
largeLists: [],
};
expect(filter).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* Side Public License, v 1.
*/

import { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { DataViewFieldBase } from '@kbn/es-query';
import { typeMatch } from '../type_match';
import { AutocompleteListsData } from '../field_value_lists';

/**
* Given an array of lists and optionally a field this will return all
Expand All @@ -21,13 +21,20 @@ import { typeMatch } from '../type_match';
* @param field The field to check against the list to see if they are compatible
*/
export const filterFieldToList = (
lists: ListSchema[],
lists: AutocompleteListsData,
field?: DataViewFieldBase & { esTypes?: string[] }
): ListSchema[] => {
): AutocompleteListsData => {
if (field != null) {
const { esTypes = [] } = field;
return lists.filter(({ type }) => esTypes.some((esType: string) => typeMatch(type, esType)));
return {
smallLists: lists.smallLists.filter(({ type }) =>
esTypes.some((esType: string) => typeMatch(type, esType))
),
largeLists: lists.largeLists.filter(({ type }) =>
esTypes.some((esType: string) => typeMatch(type, esType))
),
};
} else {
return [];
return { smallLists: [], largeLists: [] };
}
};
Loading

0 comments on commit 51699fa

Please sign in to comment.