Skip to content

Commit

Permalink
Value lists modal window (#179339)
Browse files Browse the repository at this point in the history
##  Value List Items Modal Window

Experimental flag: **valueListItemsModal**

it's temporarily true and will be set to false, before merge


https://github.com/elastic/kibana/assets/7609147/2aafe007-734f-4c0e-b542-5eca229e310d

Added a new modal window that lets users interact with value lists
directly. This modal supports viewing, searching, adding, deleting,
updating, and uploading items.

Where to find the modal window:
- Value list popup
- Exception editing and viewing on both the rule page and shared
exceptions management page
Permissions:

Add, edit, delete, and upload buttons are hidden for read-only users.
The modal link is not shown to users without read permissions.


### Changes to API
I added optional **refresh** flag for list items API, because we want to
see the changes in UI, when editing list items

### How to review

All new components is here -
`x-pack/plugins/security_solution/public/value_list/`
New hooks - `packages/kbn-securitysolution-list-hooks/`
API calls - `packages/kbn-securitysolution-list-api`
Actual API - `x-pack/plugins/lists/server/routes`

There also some props drilling into exceptions package, which is not
ideal, please let me know if you have better ideas

[test plan](elastic/security-team#9128)

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
nkhristinin and kibanamachine authored Apr 15, 2024
1 parent 9a9fd49 commit 35dc121
Show file tree
Hide file tree
Showing 80 changed files with 2,346 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import {
// const mockKibanaHttpService = coreMock.createStart().http;
// import { coreMock } from '../../../../../../../src/core/public/mocks';
const mockKibanaHttpService = jest.fn();

const mockShowValueListModal = jest.fn();
const MockedShowValueListModal = (props: unknown) => {
mockShowValueListModal(props);
return <></>;
};
const mockStart = jest.fn();
const mockKeywordList: ListSchema = {
...getListResponseMock(),
Expand Down Expand Up @@ -63,6 +67,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
showValueListModal={MockedShowValueListModal}
/>
);

Expand All @@ -84,6 +89,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
showValueListModal={MockedShowValueListModal}
/>
);

Expand Down Expand Up @@ -111,6 +117,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
showValueListModal={MockedShowValueListModal}
/>
);
expect(
Expand All @@ -131,6 +138,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
showValueListModal={MockedShowValueListModal}
/>
);

Expand All @@ -154,6 +162,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
showValueListModal={MockedShowValueListModal}
/>
);

Expand All @@ -177,6 +186,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
showValueListModal={MockedShowValueListModal}
/>
);

Expand All @@ -200,6 +210,7 @@ describe('AutocompleteFieldListsComponent', () => {
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
showValueListModal={MockedShowValueListModal}
/>
);

Expand Down Expand Up @@ -230,4 +241,29 @@ describe('AutocompleteFieldListsComponent', () => {
});
});
});

test('it render the value list modal', async () => {
mockShowValueListModal.mockReset();
mount(
<AutocompleteFieldListsComponent
httpService={mockKibanaHttpService}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={jest.fn()}
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue="some-list-id"
showValueListModal={MockedShowValueListModal}
/>
);

expect(mockShowValueListModal).toHaveBeenCalledWith(
expect.objectContaining({
children: 'Show value list',
listId: 'some-list-id',
shouldShowContentIfModalNotAvailable: false,
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { ElementType, useCallback, useEffect, useMemo, useState } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } from '@elastic/eui';
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { useFindListsBySize } from '@kbn/securitysolution-list-hooks';
Expand Down Expand Up @@ -36,6 +36,7 @@ interface AutocompleteFieldListsProps {
selectedValue: string | undefined;
allowLargeValueLists?: boolean;
'aria-label'?: string;
showValueListModal: ElementType;
}

export interface AutocompleteListsData {
Expand All @@ -55,6 +56,7 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
selectedValue,
allowLargeValueLists = false,
'aria-label': ariaLabel,
showValueListModal,
}): JSX.Element => {
const [error, setError] = useState<string | undefined>(undefined);
const [listData, setListData] = useState<AutocompleteListsData>({
Expand Down Expand Up @@ -118,29 +120,36 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
}, [selectedField, start, httpService]);

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

const helpText = useMemo(() => {
return (
!allowLargeValueLists && (
<EuiText size="xs">
{i18n.LISTS_TOOLTIP_INFO}{' '}
<EuiLink
external
target="_blank"
href={
getDocLinks({
kibanaBranch: 'main',
buildFlavor: 'traditional',
}).securitySolution.exceptions.value_lists
}
>
{i18n.SEE_DOCUMENTATION}
</EuiLink>
</EuiText>
)
<>
{selectedValue && (
<ShowValueListModal shouldShowContentIfModalNotAvailable={false} listId={selectedValue}>
{i18n.SHOW_VALUE_LIST_MODAL}
</ShowValueListModal>
)}
{!allowLargeValueLists && (
<EuiText size="xs">
{i18n.LISTS_TOOLTIP_INFO}{' '}
<EuiLink
external
target="_blank"
href={
getDocLinks({
kibanaBranch: 'main',
buildFlavor: 'traditional',
}).securitySolution.exceptions.value_lists
}
>
{i18n.SEE_DOCUMENTATION}
</EuiLink>
</EuiText>
)}
</>
);
}, [allowLargeValueLists]);

}, [allowLargeValueLists, selectedValue, ShowValueListModal]);
return (
<EuiFormRow
label={rowLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const CONFLICT_MULTIPLE_INDEX_DESCRIPTION = (name: string, count: number)
values: { count, name },
});

export const SHOW_VALUE_LIST_MODAL = i18n.translate('autocomplete.showValueListModal', {
defaultMessage: 'Show value list',
});

// eslint-disable-next-line import/no-default-export
export default {
LOADING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { render } from '@testing-library/react';
import React from 'react';

import { ExceptionItemCardConditions } from '.';
import { MockedShowValueListModal } from '../../mocks/value_list_modal.mock';

interface TestEntry {
field: string;
Expand Down Expand Up @@ -94,6 +95,7 @@ describe('ExceptionItemCardConditions', () => {
},
]}
dataTestSubj="exceptionItemConditions"
showValueListModal={MockedShowValueListModal}
/>
);
expect(wrapper.getByTestId('exceptionItemConditionsOs')).toHaveTextContent('OSIS Linux');
Expand Down Expand Up @@ -243,6 +245,7 @@ describe('ExceptionItemCardConditions', () => {
type: 'nested',
},
]}
showValueListModal={MockedShowValueListModal}
dataTestSubj="exceptionItemConditions"
/>
);
Expand Down Expand Up @@ -331,6 +334,7 @@ describe('ExceptionItemCardConditions', () => {
type: 'list',
},
]}
showValueListModal={MockedShowValueListModal}
dataTestSubj="exceptionItemConditions"
/>
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
includedListTypeEntry,
includedMatchTypeEntry,
} from '../../../mocks/entry.mock';
import {
MockedShowValueListModal,
mockShowValueListModal,
} from '../../../mocks/value_list_modal.mock';

describe('entry_content.helper', () => {
describe('getEntryOperator', () => {
Expand Down Expand Up @@ -62,36 +66,79 @@ describe('entry_content.helper', () => {
describe('getValueExpression', () => {
it('should render multiple values in badges when operator type is match_any and values is Array', () => {
const wrapper = render(
getValueExpression(ListOperatorTypeEnum.MATCH_ANY, 'included', ['value 1', 'value 2'])
getValueExpression(
ListOperatorTypeEnum.MATCH_ANY,
'included',
['value 1', 'value 2'],
MockedShowValueListModal
)
);
expect(wrapper.getByTestId('matchAnyBadge0')).toHaveTextContent('value 1');
expect(wrapper.getByTestId('matchAnyBadge1')).toHaveTextContent('value 2');
});
it('should return one value when operator type is match_any and values is not Array', () => {
const wrapper = render(
getValueExpression(ListOperatorTypeEnum.MATCH_ANY, 'included', 'value 1')
getValueExpression(
ListOperatorTypeEnum.MATCH_ANY,
'included',
'value 1',
MockedShowValueListModal
)
);
expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('value 1');
});
it('should return one value when operator type is a single value', () => {
const wrapper = render(
getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', 'value 1')
getValueExpression(
ListOperatorTypeEnum.EXISTS,
'included',
'value 1',
MockedShowValueListModal
)
);
expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent('value 1');
});
it('should return value with warning icon when the value contains a leading or trailing space', () => {
const wrapper = render(
getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', ' value 1')
getValueExpression(
ListOperatorTypeEnum.EXISTS,
'included',
' value 1',
MockedShowValueListModal
)
);
expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent(' value 1');
expect(wrapper.getByTestId('valueWithSpaceWarningTooltip')).toBeInTheDocument();
});
it('should return value without warning icon when the value does not contain a leading or trailing space', () => {
const wrapper = render(
getValueExpression(ListOperatorTypeEnum.EXISTS, 'included', 'value 1')
getValueExpression(
ListOperatorTypeEnum.EXISTS,
'included',
'value 1',
MockedShowValueListModal
)
);
expect(wrapper.getByTestId('entryValueExpression')).toHaveTextContent(' value 1');
expect(wrapper.queryByTestId('valueWithSpaceWarningTooltip')).not.toBeInTheDocument();
});
it('should render value list modal when operator type is list', () => {
mockShowValueListModal.mockReset();
render(
getValueExpression(
ListOperatorTypeEnum.LIST,
'included',
'value 1',
MockedShowValueListModal
)
);
expect(mockShowValueListModal).toHaveBeenCalledWith(
expect.objectContaining({
children: 'value 1',
listId: 'value 1',
shouldShowContentIfModalNotAvailable: true,
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import React from 'react';
import React, { ElementType } from 'react';
import { css } from '@emotion/css';
import { EuiExpression, EuiBadge } from '@elastic/eui';
import type { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
Expand All @@ -21,13 +21,21 @@ const entryValueWrapStyle = css`
const EntryValueWrap = ({ children }: { children: React.ReactNode }) => (
<span className={entryValueWrapStyle}>{children}</span>
);
const getEntryValue = (type: string, value?: string | string[]) => {

const getEntryValue = (type: string, value: string | string[], showValueListModal: ElementType) => {
const ShowValueListModal = showValueListModal;
if (type === 'match_any' && Array.isArray(value)) {
return value.map((currentValue, index) => (
<EuiBadge key={index} data-test-subj={`matchAnyBadge${index}`} color="hollow">
<EntryValueWrap>{currentValue}</EntryValueWrap>
</EuiBadge>
));
} else if (type === 'list' && value) {
return (
<ShowValueListModal shouldShowContentIfModalNotAvailable listId={value.toString()}>
{value}
</ShowValueListModal>
);
}
return <EntryValueWrap>{value}</EntryValueWrap> ?? '';
};
Expand All @@ -48,12 +56,13 @@ export const getValue = (entry: Entry) => {
export const getValueExpression = (
type: ListOperatorTypeEnum,
operator: string,
value: string | string[]
value: string | string[],
showValueListModal: ElementType
) => (
<>
<EuiExpression
description={getEntryOperator(type, operator)}
value={getEntryValue(type, value)}
value={getEntryValue(type, value, showValueListModal)}
data-test-subj="entryValueExpression"
/>
<ValueWithSpaceWarning value={value} />
Expand Down
Loading

0 comments on commit 35dc121

Please sign in to comment.