[Security Solution][Exceptions] Complete List-Header's use-cases (#144383)

* kbn add more tests + enable user to export even when readonly is true

* fix test

* fix onblur one another field removes required of another

* add tests

* fix test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wafaa Nasr 2022-11-04 13:24:44 +01:00 committed by GitHub
parent 92251b2567
commit d2c5ce8274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2374 additions and 512 deletions

View file

@ -1,5 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExceptionItemCardComments should not render comments when the length is falsy 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`ExceptionItemCardComments should render comments panel closed 1`] = `
Object {
"asFragment": [Function],

View file

@ -13,6 +13,15 @@ import * as i18n from '../translations';
const comments = mockGetFormattedComments();
describe('ExceptionItemCardComments', () => {
it('should not render comments when the length is falsy', () => {
const wrapper = render(
<ExceptionItemCardComments comments={[]} dataTestSubj="ExceptionItemCardCommentsContainer" />
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.queryByTestId('ExceptionItemCardCommentsContainer')).not.toBeInTheDocument();
});
it('should render comments panel closed', () => {
const wrapper = render(
<ExceptionItemCardComments

View file

@ -24,6 +24,7 @@ export interface ExceptionItemCardCommentsProps {
export const ExceptionItemCardComments = memo<ExceptionItemCardCommentsProps>(
({ comments, dataTestSubj }) => {
if (!comments.length) return null;
return (
<EuiFlexItem data-test-subj={dataTestSubj}>
<EuiAccordion

View file

@ -79,7 +79,7 @@ describe('ExceptionItemCardMetaInfo', () => {
item={getExceptionListItemSchemaMock()}
rules={[
{
exception_lists: [
exception_list: [
{
id: '123',
list_id: 'i_exist',
@ -98,7 +98,7 @@ describe('ExceptionItemCardMetaInfo', () => {
rule_id: 'rule-2',
},
{
exception_lists: [
exception_list: [
{
id: '123',
list_id: 'i_exist',

View file

@ -1,5 +1,201 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderMenu should render button icon disabled 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="Items"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="ButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
<div
data-euiportal="true"
>
<div
data-eui="EuiFocusTrap"
>
<div
aria-describedby="generated-id"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel"
data-popover-panel="true"
role="dialog"
style="top: 16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
>
<div
class="emotion-euiPopoverArrow-bottom"
data-popover-arrow="bottom"
style="left: 10px; top: 0px;"
/>
<p
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
You are in a dialog. To close this dialog, hit escape.
</p>
<div>
<div
class="euiContextMenuPanel"
data-test-subj="MenuPanel"
tabindex="-1"
>
<div>
<button
class="euiContextMenuItem euiContextMenuItem--small"
data-test-subj="ActionItemedit"
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="pencil"
/>
<span
class="euiContextMenuItem__text"
>
Edit detection exception
</span>
</span>
</button>
<button
class="euiContextMenuItem euiContextMenuItem--small euiContextMenuItem-isDisabled"
data-test-subj="ActionItemdelete"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="trash"
/>
<span
class="euiContextMenuItem__text"
>
Delete detection exception
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="Items"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="ButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`HeaderMenu should render button icon with default settings 1`] = `
Object {
"asFragment": [Function],

View file

@ -8,7 +8,7 @@
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { HeaderMenu } from '.';
import { actions } from '../mocks/header.mock';
import { actions, actionsWithDisabledDelete } from '../mocks/header.mock';
import { getSecurityLinkAction } from '../mocks/security_link_component.mock';
describe('HeaderMenu', () => {
@ -21,6 +21,16 @@ describe('HeaderMenu', () => {
expect(wrapper.queryByTestId('EmptyButton')).not.toBeInTheDocument();
expect(wrapper.queryByTestId('MenuPanel')).not.toBeInTheDocument();
});
it('should render button icon disabled', () => {
const wrapper = render(
<HeaderMenu disableActions={false} actions={actionsWithDisabledDelete} />
);
fireEvent.click(wrapper.getByTestId('ButtonIcon'));
expect(wrapper).toMatchSnapshot();
expect(wrapper.getByTestId('ActionItemdelete')).toBeDisabled();
expect(wrapper.getByTestId('ActionItemedit')).toBeEnabled();
});
it('should render empty button icon with different icon settings', () => {
const wrapper = render(

View file

@ -24,6 +24,7 @@ interface Action {
key: string;
icon: string;
label: string | boolean;
disabled?: boolean;
onClick: () => void;
}
interface HeaderMenuComponentProps {
@ -63,6 +64,7 @@ const HeaderMenuComponent: FC<HeaderMenuComponentProps> = ({
data-test-subj={`${dataTestSubj || ''}ActionItem${action.key}`}
key={action.key}
icon={action.icon}
disabled={action.disabled}
layoutAlign="center"
onClick={() => {
onClosePopover();

View file

@ -57,13 +57,14 @@ Object {
id="modalForm_generated-id"
>
<div
class="euiFormRow euiFormRow--hasLabel"
class="euiFormRow euiFormRow--fullWidth euiFormRow--hasLabel"
id="generated-id-row"
>
<div
class="euiFormRow__labelWrapper"
>
<label
aria-invalid="false"
class="euiFormLabel euiFormRow__label"
for="generated-id"
id="generated-id-label"
@ -75,13 +76,14 @@ Object {
class="euiFormRow__fieldWrapper"
>
<div
class="euiFormControlLayout"
class="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<input
class="euiFieldText"
aria-invalid="false"
class="euiFieldText euiFieldText--fullWidth"
data-test-subj="editModalNameTextField"
id="generated-id"
name="name"
@ -93,7 +95,7 @@ Object {
</div>
</div>
<div
class="euiFormRow euiFormRow--hasLabel"
class="euiFormRow euiFormRow--fullWidth euiFormRow--hasLabel"
id="generated-id-row"
>
<div
@ -110,22 +112,15 @@ Object {
<div
class="euiFormRow__fieldWrapper"
>
<div
class="euiFormControlLayout"
<textarea
class="euiTextArea euiTextArea--resizeVertical euiTextArea--fullWidth"
data-test-subj="editModalDescriptionTextField"
id="generated-id"
name="description"
rows="6"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<input
class="euiFieldText"
data-test-subj="editModalDescriptionTextField"
id="generated-id"
name="description"
type="text"
value="list description"
/>
</div>
</div>
list description
</textarea>
</div>
</div>
</form>

View file

@ -7,12 +7,14 @@
*/
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import * as i18n from '../../translations';
import { EditModal } from '.';
const onSave = jest.fn();
const onCancel = jest.fn();
describe('EditModal', () => {
beforeEach(() => jest.clearAllMocks());
it('should render the title and description from listDetails', () => {
const wrapper = render(
<EditModal
@ -24,7 +26,7 @@ describe('EditModal', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.getByTestId('editModalTitle')).toHaveTextContent('list name');
});
it('should call onSave', () => {
it('should call onSave when submitting the form', () => {
const wrapper = render(
<EditModal
listDetails={{ name: 'list name', description: 'list description' }}
@ -33,8 +35,10 @@ describe('EditModal', () => {
/>
);
fireEvent.submit(wrapper.getByTestId('editModalForm'));
expect(wrapper.getByTestId('editModalProgess')).toBeInTheDocument();
expect(onSave).toBeCalled();
});
it('should call onCancel', () => {
const wrapper = render(
<EditModal
@ -47,7 +51,7 @@ describe('EditModal', () => {
expect(onCancel).toBeCalled();
});
it('should call change title, description and call onSave with the new props', () => {
it('should change title, description and call onSave with the new props', () => {
const wrapper = render(
<EditModal
listDetails={{ name: 'list name', description: 'list description' }}
@ -68,4 +72,86 @@ describe('EditModal', () => {
description: 'New description name',
});
});
it('should trim title, description before calling onSave', () => {
const wrapper = render(
<EditModal
listDetails={{ name: ' list name', description: 'list description ' }}
onSave={onSave}
onCancel={onCancel}
/>
);
fireEvent.change(wrapper.getByTestId('editModalNameTextField'), {
target: { value: 'New list name' },
});
fireEvent.change(wrapper.getByTestId('editModalDescriptionTextField'), {
target: { value: 'New description name' },
});
fireEvent.submit(wrapper.getByTestId('editModalForm'));
expect(onSave).toBeCalledWith({
name: 'New list name',
description: 'New description name',
});
});
it('should not call onSave when submitting the form with invalid name field', () => {
const wrapper = render(
<EditModal
listDetails={{ name: 'list name', description: 'list description' }}
onSave={onSave}
onCancel={onCancel}
/>
);
const nameField = wrapper.getByTestId('editModalNameTextField');
fireEvent.change(nameField, {
target: { value: '' },
});
fireEvent.blur(nameField);
expect(nameField).toBeInvalid();
expect(wrapper.queryByTestId('editModalProgess')).not.toBeInTheDocument();
fireEvent.submit(wrapper.getByTestId('editModalForm'));
expect(onSave).not.toBeCalled();
expect(wrapper.getByText(i18n.LIST_NAME_REQUIRED_ERROR)).toBeTruthy();
});
it('should not call onSave when submitting the form when name field is invalid even after changing the description', () => {
const wrapper = render(
<EditModal
listDetails={{ name: 'list name', description: 'list description' }}
onSave={onSave}
onCancel={onCancel}
/>
);
const nameField = wrapper.getByTestId('editModalNameTextField');
fireEvent.change(nameField, {
target: { value: ' ' },
});
fireEvent.blur(nameField);
expect(nameField).toBeInvalid();
const descriptionField = wrapper.getByTestId('editModalDescriptionTextField');
fireEvent.change(descriptionField, {
target: { value: 'new description' },
});
fireEvent.blur(descriptionField);
expect(nameField).toBeInvalid();
expect(descriptionField).toBeValid();
expect(wrapper.queryByTestId('editModalProgess')).not.toBeInTheDocument();
fireEvent.submit(wrapper.getByTestId('editModalForm'));
expect(onSave).not.toBeCalled();
expect(wrapper.getByText(i18n.LIST_NAME_REQUIRED_ERROR)).toBeTruthy();
});
it('should call onCanel when clicking on close button', () => {
const wrapper = render(
<EditModal
listDetails={{ name: 'list name', description: 'list description' }}
onSave={onSave}
onCancel={onCancel}
/>
);
const closeButton = wrapper.getByLabelText('Closes this modal window');
fireEvent.click(closeButton);
expect(onCancel).toBeCalled();
});
});

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ChangeEvent, FC, useState } from 'react';
import React, { FC } from 'react';
import {
EuiButton,
EuiButtonEmpty,
@ -17,10 +17,12 @@ import {
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
useGeneratedHtmlId,
EuiTextArea,
EuiProgress,
} from '@elastic/eui';
import * as i18n from '../../translations';
import { ListDetails } from '../../types';
import { useEditModal } from './use_edit_modal';
interface EditModalProps {
listDetails: ListDetails;
@ -29,18 +31,16 @@ interface EditModalProps {
}
const EditModalComponent: FC<EditModalProps> = ({ listDetails, onSave, onCancel }) => {
const modalFormId = useGeneratedHtmlId({ prefix: 'modalForm' });
const [newListDetails, setNewListDetails] = useState(listDetails);
const onChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const { name, value } = target;
setNewListDetails({ ...newListDetails, [name]: value });
};
const onSubmit = () => {
onSave(newListDetails);
};
const { error, modalFormId, newListDetails, showProgress, onBlur, onSubmit, onChange } =
useEditModal({
listDetails,
onSave,
});
return (
<EuiModal data-test-subj="EditModal" onClose={onSubmit} initialFocus="[name=popswitch]">
<EuiModal data-test-subj="EditModal" onClose={onCancel} initialFocus="[name=popswitch]">
{showProgress && (
<EuiProgress data-test-subj="editModalProgess" size="xs" position="absolute" />
)}
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="editModalTitle">
<h1>{i18n.EXCEPTION_LIST_HEADER_EDIT_MODAL_TITLE(listDetails.name)}</h1>
@ -54,8 +54,16 @@ const EditModalComponent: FC<EditModalProps> = ({ listDetails, onSave, onCancel
component="form"
onSubmit={onSubmit}
>
<EuiFormRow label={i18n.EXCEPTION_LIST_HEADER_NAME_TEXTBOX}>
<EuiFormRow
error={error}
isInvalid={!!error}
fullWidth
label={i18n.EXCEPTION_LIST_HEADER_NAME_TEXTBOX}
>
<EuiFieldText
fullWidth
isInvalid={!!error}
onBlur={onBlur}
data-test-subj="editModalNameTextField"
name="name"
value={newListDetails.name}
@ -63,12 +71,14 @@ const EditModalComponent: FC<EditModalProps> = ({ listDetails, onSave, onCancel
/>
</EuiFormRow>
<EuiFormRow label={i18n.EXCEPTION_LIST_HEADER_DESCRIPTION_TEXTBOX}>
<EuiFieldText
<EuiFormRow fullWidth label={i18n.EXCEPTION_LIST_HEADER_DESCRIPTION_TEXTBOX}>
<EuiTextArea
fullWidth
data-test-subj="editModalDescriptionTextField"
name="description"
value={newListDetails.description}
onChange={onChange}
onBlur={onBlur}
/>
</EuiFormRow>
</EuiForm>

View file

@ -0,0 +1,43 @@
/*
* 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 { ChangeEvent, SyntheticEvent } from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { useEditModal } from './use_edit_modal';
const listDetails = { name: 'test-name', description: 'test-description' };
const onSave = jest.fn();
describe('useEditModal', () => {
it('should return default values based on input', () => {
const { result } = renderHook(() => useEditModal({ listDetails, onSave }));
const { error, newListDetails, showProgress } = result.current;
expect(error).toBeFalsy();
expect(newListDetails).toStrictEqual({ name: 'test-name', description: 'test-description' });
expect(showProgress).toBeFalsy();
});
it('should set error when required field is empty', () => {
const { result } = renderHook(() => useEditModal({ listDetails: { name: 'name' }, onSave }));
const { showProgress, onBlur } = result.current;
act(() =>
onBlur({ target: { name: 'name', value: '' } } as unknown as ChangeEvent<HTMLInputElement>)
);
expect(showProgress).toBeFalsy();
expect(result.current.newListDetails).toStrictEqual({ name: '' });
expect(result.current.error).toBeTruthy();
});
it('should call onSubmit if no errors and stop the event default', () => {
const { result } = renderHook(() => useEditModal({ listDetails, onSave }));
const { error, onSubmit } = result.current;
const preventDefault = jest.fn();
act(() => onSubmit({ preventDefault } as unknown as SyntheticEvent));
expect(error).toBeFalsy();
expect(onSave).toBeCalled();
expect(preventDefault).toBeCalled();
});
});

View file

@ -0,0 +1,54 @@
/*
* 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 { useGeneratedHtmlId } from '@elastic/eui';
import { useState, useCallback, ChangeEvent, SyntheticEvent } from 'react';
import * as i18n from '../../translations';
import { ListDetails } from '../../types';
interface UseEditModal {
listDetails: ListDetails;
onSave: (newListDetails: ListDetails) => void;
}
export const useEditModal = ({ listDetails, onSave }: UseEditModal) => {
const modalFormId = useGeneratedHtmlId({ prefix: 'modalForm' });
const [newListDetails, setNewListDetails] = useState<ListDetails>(listDetails);
const [showProgress, setShowProgress] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const onBlur = useCallback(
({ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
const { name, value } = target;
const trimmedValue = value.trim();
setNewListDetails({ ...newListDetails, [name]: trimmedValue });
if (name === 'name') setError(!trimmedValue ? i18n.LIST_NAME_REQUIRED_ERROR : undefined);
},
[newListDetails]
);
const onChange = ({ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = target;
setNewListDetails({ ...newListDetails, [name]: value });
};
const onSubmit = (e?: SyntheticEvent) => {
if (error) return;
setShowProgress(true);
onSave(newListDetails);
e?.preventDefault();
};
return {
error,
modalFormId,
newListDetails,
showProgress,
onBlur,
onChange,
onSubmit,
};
};

View file

@ -10,17 +10,13 @@ import React from 'react';
import type { FC } from 'react';
import { EuiIcon, EuiPageHeader, EuiText } from '@elastic/eui';
import * as i18n from '../translations';
import {
textWithEditContainerCss,
textCss,
descriptionContainerCss,
headerCss,
} from './list_header.styles';
import { textCss, descriptionContainerCss, headerCss } from './list_header.styles';
import { MenuItems } from './menu_items';
import { TextWithEdit } from '../text_with_edit';
import { EditModal } from './edit_modal';
import { ListDetails, Rule } from '../types';
import { useExceptionListHeader } from './use_list_header';
import { textWithEditContainerCss } from '../text_with_edit/text_with_edit.styles';
interface ExceptionListHeaderComponentProps {
name: string;
@ -30,6 +26,7 @@ interface ExceptionListHeaderComponentProps {
linkedRules: Rule[];
dataTestSubj?: string;
breadcrumbLink?: string;
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onEditListDetails: (listDetails: ListDetails) => void;
onExportList: () => void;
@ -46,6 +43,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
dataTestSubj,
securityLinkAnchorComponent,
breadcrumbLink,
canUserEditList = true,
onEditListDetails,
onExportList,
onDeleteList,
@ -65,7 +63,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
<TextWithEdit
dataTestSubj={`${dataTestSubj || ''}Title`}
text={listDetails.name || i18n.EXCEPTION_LIST_HEADER_NAME}
isReadonly={isReadonly}
isReadonly={isReadonly || !canUserEditList}
onEdit={onEdit}
/>
}
@ -76,7 +74,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
<TextWithEdit
dataTestSubj={`${dataTestSubj || ''}Description`}
textCss={textCss}
isReadonly={isReadonly}
isReadonly={isReadonly || !canUserEditList}
text={listDetails.description || i18n.EXCEPTION_LIST_HEADER_DESCRIPTION}
onEdit={onEdit}
/>
@ -91,6 +89,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
dataTestSubj={`${dataTestSubj || ''}RightSideMenuItems`}
linkedRules={linkedRules}
isReadonly={isReadonly}
canUserEditList={canUserEditList}
securityLinkAnchorComponent={securityLinkAnchorComponent}
onExportList={onExportList}
onDeleteList={onDeleteList}

View file

@ -17,21 +17,17 @@ export const headerMenuCss = css`
border-right: 1px solid #d3dae6;
padding: ${euiThemeVars.euiSizeXS} ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeXS} 0;
`;
export const textWithEditContainerCss = css`
display: flex;
width: fit-content;
align-items: baseline;
margin-bottom: ${euiThemeVars.euiSizeS};
h1 {
margin-bottom: 0;
}
export const noLinkedRulesCss = css`
width: max-content;
`;
export const textCss = css`
font-size: ${euiThemeVars.euiFontSize};
color: ${euiThemeVars.euiTextSubduedColor};
margin-left: ${euiThemeVars.euiSizeXS};
`;
export const descriptionContainerCss = css`
margin-top: -${euiThemeVars.euiSizeL};
margin-top: -${euiThemeVars.euiSizeXXL};
margin-bottom: -${euiThemeVars.euiSizeL};
`;

View file

@ -27,7 +27,7 @@ describe('ExceptionListHeader', () => {
onCancel: jest.fn(),
});
});
it('should render the List Header with name, default description and disabled actions because of the ReadOnly mode', () => {
it('should render the List Header with name, default description and disabled actions because of the ReadOnly mode', () => {
const wrapper = render(
<ExceptionListHeader
listId="List_Id"
@ -42,8 +42,8 @@ describe('ExceptionListHeader', () => {
/>
);
expect(wrapper).toMatchSnapshot();
fireEvent.click(wrapper.getByTestId('RightSideMenuItemsContainer'));
expect(wrapper.queryByTestId('MenuActions')).not.toBeInTheDocument();
fireEvent.click(wrapper.getByTestId('RightSideMenuItemsMenuActionsItems'));
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsButtonIcon')).toBeDisabled();
expect(wrapper.getByTestId('DescriptionText')).toHaveTextContent(
i18n.EXCEPTION_LIST_HEADER_DESCRIPTION
);
@ -55,6 +55,29 @@ describe('ExceptionListHeader', () => {
i18n.EXCEPTION_LIST_HEADER_BREADCRUMB
);
});
it('should render the List Header with name, default description and disabled actions because user can not edit details', () => {
const wrapper = render(
<ExceptionListHeader
listId="List_Id"
name="List Name"
isReadonly={false}
canUserEditList={false}
linkedRules={[]}
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
onEditListDetails={onEditListDetails}
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
/>
);
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsButtonIcon')).toBeEnabled();
fireEvent.click(wrapper.getByTestId('RightSideMenuItemsMenuActionsButtonIcon'));
expect(wrapper).toMatchSnapshot();
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsActionItem1')).toBeEnabled();
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsActionItem2')).toBeDisabled();
expect(wrapper.queryByTestId('EditTitleIcon')).not.toBeInTheDocument();
});
it('should render the List Header with name, default description and actions', () => {
const wrapper = render(
<ExceptionListHeader

View file

@ -1,5 +1,795 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MenuItems should not render linkedRules HeaderMenu component, instead should render a text 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<span
class="emotion-EuiTextColor"
data-test-subj="noLinkedRules"
>
Linked to 0 rules
</span>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<span
class="emotion-EuiTextColor"
data-test-subj="noLinkedRules"
>
Linked to 0 rules
</span>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`MenuItems should render all menu actions enabled 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<span
class="emotion-EuiTextColor"
data-test-subj="noLinkedRules"
>
Linked to 0 rules
</span>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div
data-euiportal="true"
>
<div
data-eui="EuiFocusTrap"
>
<div
aria-describedby="generated-id"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel"
data-popover-panel="true"
role="dialog"
style="top: 16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
>
<div
class="emotion-euiPopoverArrow-bottom"
data-popover-arrow="bottom"
style="left: 10px; top: 0px;"
/>
<p
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
You are in a dialog. To close this dialog, hit escape.
</p>
<div>
<div
class="euiContextMenuPanel"
data-test-subj="MenuActionsMenuPanel"
tabindex="-1"
>
<div>
<button
class="euiContextMenuItem euiContextMenuItem--small"
data-test-subj="MenuActionsActionItem1"
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="exportAction"
/>
<span
class="euiContextMenuItem__text"
>
Export exception list
</span>
</span>
</button>
<button
class="euiContextMenuItem euiContextMenuItem--small"
data-test-subj="MenuActionsActionItem2"
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="trash"
/>
<span
class="euiContextMenuItem__text"
>
Delete exception list
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<span
class="emotion-EuiTextColor"
data-test-subj="noLinkedRules"
>
Linked to 0 rules
</span>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`MenuItems should render delete action disabled 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="LinkedRulesMenuItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Empty"
class="euiButtonEmpty euiButtonEmpty--primary"
data-test-subj="LinkedRulesMenuEmptyButton"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content"
>
<span
class="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
/>
<span
class="euiButtonEmpty__text"
>
Linked to 1 rules
</span>
</span>
</button>
</div>
</div>
</div>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div
data-euiportal="true"
>
<div
data-eui="EuiFocusTrap"
>
<div
aria-describedby="generated-id"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel"
data-popover-panel="true"
role="dialog"
style="top: 16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
>
<div
class="emotion-euiPopoverArrow-bottom"
data-popover-arrow="bottom"
style="left: 10px; top: 0px;"
/>
<p
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
You are in a dialog. To close this dialog, hit escape.
</p>
<div>
<div
class="euiContextMenuPanel"
data-test-subj="MenuActionsMenuPanel"
tabindex="-1"
>
<div>
<button
class="euiContextMenuItem euiContextMenuItem--small"
data-test-subj="MenuActionsActionItem1"
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="exportAction"
/>
<span
class="euiContextMenuItem__text"
>
Export exception list
</span>
</span>
</button>
<button
class="euiContextMenuItem euiContextMenuItem--small euiContextMenuItem-isDisabled"
data-test-subj="MenuActionsActionItem2"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenu__icon"
color="inherit"
data-euiicon-type="trash"
/>
<span
class="euiContextMenuItem__text"
>
Delete exception list
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
data-test-subj="Container"
>
<div
class="euiFlexItem emotion-EuiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="LinkedRulesMenuItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Empty"
class="euiButtonEmpty euiButtonEmpty--primary"
data-test-subj="LinkedRulesMenuEmptyButton"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content"
>
<span
class="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
/>
<span
class="euiButtonEmpty__text"
>
Linked to 1 rules
</span>
</span>
</button>
</div>
</div>
</div>
</div>
<div
class="euiFlexItem"
>
<button
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="ManageRulesButton"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Manage rules
</span>
</span>
</button>
</div>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiPopover emotion-euiPopover"
data-test-subj="MenuActionsItems"
>
<div
class="euiPopover__anchor css-16vtueo-render"
>
<button
aria-label="Header menu Button Icon"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="MenuActionsButtonIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="boxesHorizontal"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`MenuItems should render linkedRules, manageRules and menuActions 1`] = `
Object {
"asFragment": [Function],

View file

@ -5,10 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui';
import React, { FC, useMemo } from 'react';
import { HeaderMenu } from '../../header_menu';
import { headerMenuCss } from '../list_header.styles';
import { headerMenuCss, noLinkedRulesCss } from '../list_header.styles';
import * as i18n from '../../translations';
import { Rule } from '../../types';
import { generateLinkedRulesMenuItems } from '../../generate_linked_rules_menu_item';
@ -16,6 +16,7 @@ interface MenuItemsProps {
isReadonly: boolean;
dataTestSubj?: string;
linkedRules: Rule[];
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onExportList: () => void;
onDeleteList: () => void;
@ -27,6 +28,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
linkedRules,
securityLinkAnchorComponent,
isReadonly,
canUserEditList = true,
onExportList,
onDeleteList,
onManageRules,
@ -51,17 +53,23 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
gutterSize="l"
>
<EuiFlexItem css={headerMenuCss}>
<HeaderMenu
dataTestSubj={`${dataTestSubj || ''}LinkedRulesMenu`}
emptyButton
useCustomActions
text={i18n.EXCEPTION_LIST_HEADER_LINKED_RULES(linkedRules.length)}
actions={referencedLinks}
disableActions={isReadonly}
iconType="arrowDown"
iconSide="right"
panelPaddingSize="none"
/>
{linkedRules.length ? (
<HeaderMenu
dataTestSubj={`${dataTestSubj || ''}LinkedRulesMenu`}
emptyButton
useCustomActions
text={i18n.EXCEPTION_LIST_HEADER_LINKED_RULES(linkedRules.length)}
actions={referencedLinks}
disableActions={false}
iconType="arrowDown"
iconSide="right"
panelPaddingSize="none"
/>
) : (
<EuiTextColor data-test-subj="noLinkedRules" css={noLinkedRulesCss} color="subdued">
{i18n.EXCEPTION_LIST_HEADER_LINKED_RULES(linkedRules.length)}
</EuiTextColor>
)}
</EuiFlexItem>
<EuiFlexItem>
@ -95,6 +103,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
onClick: () => {
if (typeof onDeleteList === 'function') onDeleteList();
},
disabled: !canUserEditList,
},
]}
disableActions={isReadonly}

View file

@ -32,6 +32,53 @@ describe('MenuItems', () => {
expect(wrapper.getByTestId('ManageRulesButton')).toBeInTheDocument();
expect(wrapper.getByTestId('MenuActionsButtonIcon')).toBeInTheDocument();
});
it('should not render linkedRules HeaderMenu component, instead should render a text', () => {
const wrapper = render(
<MenuItems
isReadonly={false}
linkedRules={[]}
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
/>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.queryByTestId('LinkedRulesMenuItems')).not.toBeInTheDocument();
expect(wrapper.getByTestId('noLinkedRules')).toBeInTheDocument();
});
it('should render all menu actions enabled', () => {
const wrapper = render(
<MenuItems
isReadonly={false}
linkedRules={[]}
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
/>
);
fireEvent.click(wrapper.getByTestId('MenuActionsButtonIcon'));
expect(wrapper).toMatchSnapshot();
expect(wrapper.getByTestId('MenuActionsActionItem1')).toBeEnabled();
expect(wrapper.getByTestId('MenuActionsActionItem2')).toBeEnabled();
});
it('should render delete action disabled', () => {
const wrapper = render(
<MenuItems
isReadonly={false}
canUserEditList={false}
linkedRules={rules}
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
/>
);
fireEvent.click(wrapper.getByTestId('MenuActionsButtonIcon'));
expect(wrapper).toMatchSnapshot();
expect(wrapper.getByTestId('MenuActionsActionItem2')).toBeDisabled();
});
it('should call onManageRules', () => {
const wrapper = render(
<MenuItems

View file

@ -24,9 +24,11 @@ export const useExceptionListHeader = ({
setIsModalVisible(true);
};
const onSave = (newListDetails: ListDetails) => {
setIsModalVisible(false);
setListDetails(newListDetails);
if (typeof onEditListDetails === 'function') onEditListDetails(newListDetails);
setTimeout(() => {
setIsModalVisible(false);
}, 200);
};
const onCancel = () => {
setIsModalVisible(false);

View file

@ -21,3 +21,18 @@ export const actions = [
onClick: handleDelete,
},
];
export const actionsWithDisabledDelete = [
{
key: 'edit',
icon: 'pencil',
label: 'Edit detection exception',
onClick: handleEdit,
},
{
key: 'delete',
icon: 'trash',
disabled: true,
label: 'Delete detection exception',
onClick: handleDelete,
},
];

View file

@ -10,7 +10,7 @@ import { Rule, RuleReference } from '../types';
export const rules: Rule[] = [
{
exception_lists: [
exception_list: [
{
id: '123',
list_id: 'i_exist',

View file

@ -26,7 +26,7 @@ export const getSecurityLinkAction = (dataTestSubj: string) =>
linkedRules: [
...rules,
{
exception_lists: [],
exception_list: [],
id: '2a2b3c',
name: 'Simple Rule Query 2',
rule_id: 'rule-2',

View file

@ -6,7 +6,29 @@ Object {
"baseElement": <body>
<div>
<div
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive css-nhr7ke"
>
<div
class="euiFlexItem euiFlexItem--flexGrow10"
>
<span
data-test-subj="TextWithEditTestText"
>
Test
</span>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero emotion-EuiFlexItem"
/>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive css-nhr7ke"
>
<div
class="euiFlexItem euiFlexItem--flexGrow10"
>
<span
data-test-subj="TextWithEditTestText"
@ -14,17 +36,9 @@ Object {
Test
</span>
</div>
</div>
</body>,
"container": <div>
<div
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
>
<span
data-test-subj="TextWithEditTestText"
>
Test
</span>
<div
class="euiFlexItem euiFlexItem--flexGrowZero emotion-EuiFlexItem"
/>
</div>
</div>,
"debug": [Function],
@ -87,13 +101,53 @@ Object {
"baseElement": <body>
<div>
<div
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive css-nhr7ke"
>
<div
class="euiFlexItem euiFlexItem--flexGrow10"
>
<span
data-test-subj="TextWithEditTestText"
>
Test
</span>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero emotion-EuiFlexItem"
>
<button
aria-label="Edit Text List Header"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="TextWithEditTestEditIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="pencil"
/>
</button>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive css-nhr7ke"
>
<div
class="euiFlexItem euiFlexItem--flexGrow10"
>
<span
data-test-subj="TextWithEditTestText"
>
Test
</span>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero emotion-EuiFlexItem"
>
<button
aria-label="Edit Text List Header"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
@ -109,30 +163,6 @@ Object {
</button>
</div>
</div>
</body>,
"container": <div>
<div
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
>
<span
data-test-subj="TextWithEditTestText"
>
Test
</span>
<button
aria-label="Edit Text List Header"
class="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="TextWithEditTestEditIcon"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="pencil"
/>
</button>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],

View file

@ -7,10 +7,9 @@
*/
import React, { FC } from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Interpolation, Theme } from '@emotion/react';
import { textWithEditContainerCss } from '../list_header/list_header.styles';
import { textWithEditContainerCss, editIconCss } from './text_with_edit.styles';
interface TextWithEditProps {
isReadonly: boolean;
dataTestSubj?: string;
@ -27,19 +26,23 @@ const TextWithEditComponent: FC<TextWithEditProps> = ({
textCss,
}) => {
return (
<div css={textWithEditContainerCss}>
<span css={textCss} data-test-subj={`${dataTestSubj || ''}Text`}>
{text}
</span>
{isReadonly ? null : (
<EuiButtonIcon
data-test-subj={`${dataTestSubj || ''}EditIcon`}
aria-label="Edit Text List Header"
iconType="pencil"
onClick={() => (typeof onEdit === 'function' ? onEdit() : null)}
/>
)}
</div>
<EuiFlexGroup css={textWithEditContainerCss}>
<EuiFlexItem grow={10}>
<span css={textCss} data-test-subj={`${dataTestSubj || ''}Text`}>
{text}
</span>
</EuiFlexItem>
<EuiFlexItem grow={false} css={editIconCss}>
{isReadonly ? null : (
<EuiButtonIcon
data-test-subj={`${dataTestSubj || ''}EditIcon`}
aria-label="Edit Text List Header"
iconType="pencil"
onClick={() => (typeof onEdit === 'function' ? onEdit() : null)}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};
TextWithEditComponent.displayName = 'TextWithEditComponent';

View file

@ -0,0 +1,25 @@
/*
* 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 { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
export const textWithEditContainerCss = css`
display: flex;
width: fit-content;
align-items: baseline;
padding-bottom: ${euiThemeVars.euiSizeS};
h1 {
margin-bottom: 0;
}
`;
export const editIconCss = css`
button {
margin-left: -${euiThemeVars.euiSizeM};
}
`;

View file

@ -140,3 +140,10 @@ export const EXCEPTION_LIST_HEADER_DESCRIPTION_TEXTBOX = i18n.translate(
defaultMessage: 'Description',
}
);
export const LIST_NAME_REQUIRED_ERROR = i18n.translate(
'exceptionList-components.exception_list_header_description_textboxexceptionList-components.exception_list_header_name_required_eror',
{
defaultMessage: 'List name cannot be empty',
}
);

View file

@ -60,7 +60,7 @@ export interface Rule {
name: string;
id: string;
rule_id: string;
exception_lists: ListArray;
exception_list?: ListArray;
}
export interface RuleReference {