[RAC] Actions popovers UI unification (#109221)

* popover padding size unified

* remove panels from all context menus

* action items order changed

* cases menu items test fixed

* translations and small changes

* remove components not used anywhere

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Angela Chuang <yi-chun.chuang@elastic.co>
This commit is contained in:
Sergi Massaneda 2021-08-23 14:42:24 +02:00 committed by GitHub
parent ec7889a938
commit 64dff78dce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 209 additions and 485 deletions

View file

@ -33,7 +33,7 @@ import {
EuiDataGridColumn, EuiDataGridColumn,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiContextMenu, EuiContextMenuPanel,
EuiPopover, EuiPopover,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -212,11 +212,8 @@ function ObservabilityActions({
onUpdateFailure: onAlertStatusUpdated, onUpdateFailure: onAlertStatusUpdated,
}); });
const actionsPanels = useMemo(() => { const actionsMenuItems = useMemo(() => {
return [ return [
{
id: 0,
content: [
timelines.getAddToExistingCaseButton({ timelines.getAddToExistingCaseButton({
event, event,
casePermissions, casePermissions,
@ -230,8 +227,6 @@ function ObservabilityActions({
onClose: afterCaseSelection, onClose: afterCaseSelection,
}), }),
...(alertPermissions.crud ? statusActionItems : []), ...(alertPermissions.crud ? statusActionItems : []),
],
},
]; ];
}, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]);
@ -255,6 +250,7 @@ function ObservabilityActions({
aria-label="View alert in app" aria-label="View alert in app"
/> />
</EuiFlexItem> </EuiFlexItem>
{actionsMenuItems.length > 0 && (
<EuiFlexItem> <EuiFlexItem>
<EuiPopover <EuiPopover
button={ button={
@ -272,9 +268,10 @@ function ObservabilityActions({
panelPaddingSize="none" panelPaddingSize="none"
anchorPosition="downLeft" anchorPosition="downLeft"
> >
<EuiContextMenu panels={actionsPanels} initialPanelId={0} /> <EuiContextMenuPanel size="s" items={actionsMenuItems} />
</EuiPopover> </EuiPopover>
</EuiFlexItem> </EuiFlexItem>
)}
</EuiFlexGroup> </EuiFlexGroup>
</> </>
); );

View file

@ -19,6 +19,14 @@ export const mockTimelines = {
.fn() .fn()
.mockReturnValue(<div data-test-subj="add-to-case-action">{'Add to case'}</div>), .mockReturnValue(<div data-test-subj="add-to-case-action">{'Add to case'}</div>),
getAddToCaseAction: jest.fn(), getAddToCaseAction: jest.fn(),
getAddToExistingCaseButton: jest.fn(), getAddToExistingCaseButton: jest.fn().mockReturnValue(
getAddToNewCaseButton: jest.fn(), <div key="add-to-existing-case-action" data-test-subj="add-to-existing-case-action">
{'Add to existing case'}
</div>
),
getAddToNewCaseButton: jest.fn().mockReturnValue(
<div key="add-to-new-case-action" data-test-subj="add-to-new-case-action">
{'Add to new case'}
</div>
),
}; };

View file

@ -1,36 +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 { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
interface AddEndpointExceptionProps {
onClick: () => void;
disabled?: boolean;
}
const AddEndpointExceptionComponent: React.FC<AddEndpointExceptionProps> = ({
onClick,
disabled,
}) => {
return (
<EuiContextMenuItem
key="add-endpoint-exception-menu-item"
aria-label={i18n.ACTION_ADD_ENDPOINT_EXCEPTION}
data-test-subj="add-endpoint-exception-menu-item"
id="addEndpointException"
onClick={onClick}
disabled={disabled}
size="s"
>
{i18n.ACTION_ADD_ENDPOINT_EXCEPTION}
</EuiContextMenuItem>
);
};
export const AddEndpointException = React.memo(AddEndpointExceptionComponent);

View file

@ -1,32 +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 { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
interface AddEventFilterProps {
onClick: () => void;
disabled?: boolean;
}
const AddEventFilterComponent: React.FC<AddEventFilterProps> = ({ onClick, disabled }) => {
return (
<EuiContextMenuItem
key="add-event-filter-menu-item"
aria-label={i18n.ACTION_ADD_EVENT_FILTER}
data-test-subj="add-event-filter-menu-item"
id="addEventFilter"
onClick={onClick}
disabled={disabled}
>
{i18n.ACTION_ADD_EVENT_FILTER}
</EuiContextMenuItem>
);
};
export const AddEventFilter = React.memo(AddEventFilterComponent);

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
interface AddExceptionProps {
disabled?: boolean;
onClick: () => void;
}
const AddExceptionComponent: React.FC<AddExceptionProps> = ({ disabled, onClick }) => {
return (
<EuiContextMenuItem
key="add-exception-menu-item"
aria-label={i18n.ACTION_ADD_EXCEPTION}
data-test-subj="add-exception-menu-item"
id="addException"
onClick={onClick}
disabled={disabled}
size="s"
>
{i18n.ACTION_ADD_EXCEPTION}
</EuiContextMenuItem>
);
};
export const AddException = React.memo(AddExceptionComponent);

View file

@ -56,7 +56,8 @@ jest.mock('../../../../common/lib/kibana', () => ({
})); }));
const actionMenuButton = '[data-test-subj="timeline-context-menu-button"] button'; const actionMenuButton = '[data-test-subj="timeline-context-menu-button"] button';
const addToCaseButton = '[data-test-subj="attach-alert-to-case-button"]'; const addToExistingCaseButton = '[data-test-subj="add-to-existing-case-action"]';
const addToNewCaseButton = '[data-test-subj="add-to-new-case-action"]';
describe('InvestigateInResolverAction', () => { describe('InvestigateInResolverAction', () => {
test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => { test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => {
@ -65,7 +66,8 @@ describe('InvestigateInResolverAction', () => {
}); });
wrapper.find(actionMenuButton).simulate('click'); wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true); expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true);
expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true);
}); });
test('it render AddToCase context menu item if timelineId === TimelineId.detectionsRulesDetailsPage', () => { test('it render AddToCase context menu item if timelineId === TimelineId.detectionsRulesDetailsPage', () => {
@ -77,7 +79,8 @@ describe('InvestigateInResolverAction', () => {
); );
wrapper.find(actionMenuButton).simulate('click'); wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true); expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true);
expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true);
}); });
test('it render AddToCase context menu item if timelineId === TimelineId.active', () => { test('it render AddToCase context menu item if timelineId === TimelineId.active', () => {
@ -86,7 +89,8 @@ describe('InvestigateInResolverAction', () => {
}); });
wrapper.find(actionMenuButton).simulate('click'); wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true); expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(true);
expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(true);
}); });
test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => { test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => {
@ -94,6 +98,7 @@ describe('InvestigateInResolverAction', () => {
wrappingComponent: TestProviders, wrappingComponent: TestProviders,
}); });
wrapper.find(actionMenuButton).simulate('click'); wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(false); expect(wrapper.find(addToExistingCaseButton).first().exists()).toEqual(false);
expect(wrapper.find(addToNewCaseButton).first().exists()).toEqual(false);
}); });
}); });

View file

@ -7,16 +7,11 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui'; import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui';
import { indexOf } from 'lodash'; import { indexOf } from 'lodash';
import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { get, getOr } from 'lodash/fp'; import { get, getOr } from 'lodash/fp';
import {
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui/src/components/context_menu/context_menu';
import styled from 'styled-components';
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers';
@ -54,11 +49,6 @@ interface AlertContextMenuProps {
onRuleChange?: () => void; onRuleChange?: () => void;
timelineId: string; timelineId: string;
} }
export const NestedWrapper = styled.span`
button.euiContextMenuItem {
padding: 0;
}
`;
const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
ariaLabel = i18n.MORE_ACTIONS, ariaLabel = i18n.MORE_ACTIONS,
@ -99,27 +89,18 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
] ]
); );
const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false; const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
const addToCaseAction = useMemo( const addToCaseActionItems = useMemo(
() => () =>
[ [
TimelineId.detectionsPage, TimelineId.detectionsPage,
TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsRulesDetailsPage,
TimelineId.active, TimelineId.active,
].includes(timelineId as TimelineId) && hasWritePermissions ].includes(timelineId as TimelineId) && hasWritePermissions
? { ? [
actionItem: [
{
name: i18n.ACTION_ADD_TO_CASE,
panel: 2,
'data-test-subj': 'attach-alert-to-case-button',
},
],
content: [
timelinesUi.getAddToExistingCaseButton(addToCaseActionProps), timelinesUi.getAddToExistingCaseButton(addToCaseActionProps),
timelinesUi.getAddToNewCaseButton(addToCaseActionProps), timelinesUi.getAddToNewCaseButton(addToCaseActionProps),
], ]
} : [],
: { actionItem: [], content: [] },
[addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi] [addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi]
); );
@ -179,7 +160,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
onAddEventFilterClick, onAddEventFilterClick,
} = useEventFilterModal(); } = useEventFilterModal();
const { actionItems } = useAlertsActions({ const { actionItems: statusActionItems } = useAlertsActions({
alertStatus, alertStatus,
eventId: ecsRowData?._id, eventId: ecsRowData?._id,
indexName: ecsRowData?._index ?? '', indexName: ecsRowData?._index ?? '',
@ -201,57 +182,43 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
closePopover(); closePopover();
}, [closePopover, onAddEventFilterClick]); }, [closePopover, onAddEventFilterClick]);
const { exceptionActions } = useExceptionActions({ const { exceptionActionItems } = useExceptionActions({
isEndpointAlert, isEndpointAlert,
onAddExceptionTypeClick: handleOnAddExceptionTypeClick, onAddExceptionTypeClick: handleOnAddExceptionTypeClick,
}); });
const investigateInResolverAction = useInvestigateInResolverContextItem({ const investigateInResolverActionItems = useInvestigateInResolverContextItem({
timelineId, timelineId,
ecsData: ecsRowData, ecsData: ecsRowData,
onClose: afterItemSelection, onClose: afterItemSelection,
}); });
const eventFilterAction = useEventFilterAction({ const { eventFilterActionItems } = useEventFilterAction({
onAddEventFilterClick: handleOnAddEventFilterClick, onAddEventFilterClick: handleOnAddEventFilterClick,
}); });
const items: EuiContextMenuPanelItemDescriptor[] = useMemo( const items: React.ReactElement[] = useMemo(
() => () =>
!isEvent && ruleId !isEvent && ruleId
? [ ? [
...investigateInResolverAction, ...investigateInResolverActionItems,
...addToCaseAction.actionItem, ...addToCaseActionItems,
...actionItems.map((aI) => ({ name: <NestedWrapper>{aI}</NestedWrapper> })), ...statusActionItems,
...exceptionActions, ...exceptionActionItems,
] ]
: [...investigateInResolverAction, ...addToCaseAction.actionItem, eventFilterAction], : [...investigateInResolverActionItems, ...addToCaseActionItems, ...eventFilterActionItems],
[ [
actionItems, statusActionItems,
addToCaseAction.actionItem, addToCaseActionItems,
eventFilterAction, eventFilterActionItems,
exceptionActions, exceptionActionItems,
investigateInResolverAction, investigateInResolverActionItems,
isEvent, isEvent,
ruleId, ruleId,
] ]
); );
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
{
id: 0,
items,
},
{
id: 2,
title: i18n.ACTION_ADD_TO_CASE,
content: addToCaseAction.content,
},
],
[addToCaseAction.content, items]
);
return ( return (
<> <>
{timelinesUi.getAddToCaseAction(addToCaseActionProps)} {timelinesUi.getAddToCaseAction(addToCaseActionProps)}
{items.length > 0 && (
<div key="actions-context-menu"> <div key="actions-context-menu">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}> <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EuiPopover <EuiPopover
@ -263,10 +230,11 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
anchorPosition="downLeft" anchorPosition="downLeft"
repositionOnScroll repositionOnScroll
> >
<EuiContextMenu size="s" initialPanelId={0} panels={panels} /> <EuiContextMenuPanel size="s" items={items} />
</EuiPopover> </EuiPopover>
</EventsTdContent> </EventsTdContent>
</div> </div>
)}
{exceptionModalType != null && {exceptionModalType != null &&
ruleId != null && ruleId != null &&
ruleName != null && ruleName != null &&

View file

@ -1,36 +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 { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import { FILTER_ACKNOWLEDGED } from '../../alerts_filter_group';
import * as i18n from '../../translations';
interface AcknowledgedAlertStatusProps {
onClick: () => void;
disabled?: boolean;
}
const AcknowledgedAlertStatusComponent: React.FC<AcknowledgedAlertStatusProps> = ({
onClick,
disabled,
}) => {
return (
<EuiContextMenuItem
key="acknowledged-alert"
aria-label={i18n.ACTION_ACKNOWLEDGED_ALERT}
data-test-subj="acknowledged-alert-status"
id={FILTER_ACKNOWLEDGED}
onClick={onClick}
disabled={disabled}
>
{i18n.ACTION_ACKNOWLEDGED_ALERT}
</EuiContextMenuItem>
);
};
export const AcknowledgedAlertStatus = React.memo(AcknowledgedAlertStatusComponent);

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import { FILTER_CLOSED } from '../../alerts_filter_group';
import * as i18n from '../../translations';
interface CloseAlertActionProps {
onClick: () => void;
disabled?: boolean;
}
const CloseAlertActionComponent: React.FC<CloseAlertActionProps> = ({ onClick, disabled }) => {
return (
<EuiContextMenuItem
key="close-alert"
aria-label={i18n.ACTION_CLOSE_ALERT}
data-test-subj="close-alert-status"
id={FILTER_CLOSED}
onClick={onClick}
disabled={disabled}
>
{i18n.ACTION_CLOSE_ALERT}
</EuiContextMenuItem>
);
};
export const CloseAlertAction = React.memo(CloseAlertActionComponent);

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import { FILTER_OPEN } from '../../alerts_filter_group';
import * as i18n from '../../translations';
interface OpenAlertStatusProps {
onClick: () => void;
disabled?: boolean;
}
const OpenAlertStatusComponent: React.FC<OpenAlertStatusProps> = ({ onClick, disabled }) => {
return (
<EuiContextMenuItem
key="open-alert"
aria-label={i18n.ACTION_OPEN_ALERT}
data-test-subj="open-alert-status"
id={FILTER_OPEN}
onClick={onClick}
disabled={disabled}
>
{i18n.ACTION_OPEN_ALERT}
</EuiContextMenuItem>
);
};
export const OpenAlertStatus = React.memo(OpenAlertStatusComponent);

View file

@ -5,8 +5,9 @@
* 2.0. * 2.0.
*/ */
import { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { EuiContextMenuItem } from '@elastic/eui';
import { get } from 'lodash/fp'; import { get } from 'lodash/fp';
import { import {
setActiveTabTimeline, setActiveTabTimeline,
@ -44,9 +45,12 @@ export const useInvestigateInResolverContextItem = ({
return isDisabled return isDisabled
? [] ? []
: [ : [
{ <EuiContextMenuItem
name: ACTION_INVESTIGATE_IN_RESOLVER, key="investigate-in-resolver-action-item"
onClick: handleClick, data-test-subj="investigate-in-resolver-action-item"
}, onClick={handleClick}
>
{ACTION_INVESTIGATE_IN_RESOLVER}
</EuiContextMenuItem>,
]; ];
}; };

View file

@ -5,26 +5,13 @@
* 2.0. * 2.0.
*/ */
import { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { useUserData } from '../../user_info'; import { useUserData } from '../../user_info';
import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations'; import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations';
interface ExceptionActions {
name: string;
onClick: () => void;
disabled: boolean;
}
interface UseExceptionActions {
disabledAddEndpointException: boolean;
disabledAddException: boolean;
exceptionActions: ExceptionActions[];
handleEndpointExceptionModal: () => void;
handleDetectionExceptionModal: () => void;
}
interface UseExceptionActionProps { interface UseExceptionActionProps {
isEndpointAlert: boolean; isEndpointAlert: boolean;
onAddExceptionTypeClick: (type: ExceptionListType) => void; onAddExceptionTypeClick: (type: ExceptionListType) => void;
@ -33,7 +20,7 @@ interface UseExceptionActionProps {
export const useExceptionActions = ({ export const useExceptionActions = ({
isEndpointAlert, isEndpointAlert,
onAddExceptionTypeClick, onAddExceptionTypeClick,
}: UseExceptionActionProps): UseExceptionActions => { }: UseExceptionActionProps) => {
const [{ canUserCRUD, hasIndexWrite }] = useUserData(); const [{ canUserCRUD, hasIndexWrite }] = useUserData();
const handleDetectionExceptionModal = useCallback(() => { const handleDetectionExceptionModal = useCallback(() => {
@ -47,20 +34,25 @@ export const useExceptionActions = ({
const disabledAddEndpointException = !canUserCRUD || !hasIndexWrite || !isEndpointAlert; const disabledAddEndpointException = !canUserCRUD || !hasIndexWrite || !isEndpointAlert;
const disabledAddException = !canUserCRUD || !hasIndexWrite; const disabledAddException = !canUserCRUD || !hasIndexWrite;
const exceptionActions = useMemo( const exceptionActionItems = useMemo(
() => [ () => [
{ <EuiContextMenuItem
name: ACTION_ADD_ENDPOINT_EXCEPTION, key="add-endpoint-exception-menu-item"
onClick: handleEndpointExceptionModal, data-test-subj="add-endpoint-exception-menu-item"
disabled: disabledAddEndpointException, disabled={disabledAddEndpointException}
[`data-test-subj`]: 'add-endpoint-exception-menu-item', onClick={handleEndpointExceptionModal}
}, >
{ {ACTION_ADD_ENDPOINT_EXCEPTION}
name: ACTION_ADD_EXCEPTION, </EuiContextMenuItem>,
onClick: handleDetectionExceptionModal,
disabled: disabledAddException, <EuiContextMenuItem
[`data-test-subj`]: 'add-exception-menu-item', key="add-exception-menu-item"
}, data-test-subj="add-exception-menu-item"
disabled={disabledAddException}
onClick={handleDetectionExceptionModal}
>
{ACTION_ADD_EXCEPTION}
</EuiContextMenuItem>,
], ],
[ [
disabledAddEndpointException, disabledAddEndpointException,
@ -70,11 +62,5 @@ export const useExceptionActions = ({
] ]
); );
return { return { exceptionActionItems };
disabledAddEndpointException,
disabledAddException,
exceptionActions,
handleEndpointExceptionModal,
handleDetectionExceptionModal,
};
}; };

View file

@ -5,7 +5,8 @@
* 2.0. * 2.0.
*/ */
import { useMemo } from 'react'; import React, { useMemo } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { ACTION_ADD_EVENT_FILTER } from '../translations'; import { ACTION_ADD_EVENT_FILTER } from '../translations';
export const useEventFilterAction = ({ export const useEventFilterAction = ({
@ -13,12 +14,17 @@ export const useEventFilterAction = ({
}: { }: {
onAddEventFilterClick: () => void; onAddEventFilterClick: () => void;
}) => { }) => {
const eventFilterActions = useMemo( const eventFilterActionItems = useMemo(
() => ({ () => [
name: ACTION_ADD_EVENT_FILTER, <EuiContextMenuItem
onClick: onAddEventFilterClick, key="add-event-filter-menu-item"
}), data-test-subj="add-event-filter-menu-item"
onClick={onAddEventFilterClick}
>
{ACTION_ADD_EVENT_FILTER}
</EuiContextMenuItem>,
],
[onAddEventFilterClick] [onAddEventFilterClick]
); );
return eventFilterActions; return { eventFilterActionItems };
}; };

View file

@ -4,9 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { EuiContextMenuItem } from '@elastic/eui';
import { useKibana } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineId } from '../../../../../common/types/timeline';
@ -102,18 +103,21 @@ export const useInvestigateInTimeline = ({
updateTimelineIsLoading, updateTimelineIsLoading,
]); ]);
const investigateInTimelineAction = showInvestigateInTimelineAction const investigateInTimelineActionItems = showInvestigateInTimelineAction
? [ ? [
{ <EuiContextMenuItem
name: ACTION_INVESTIGATE_IN_TIMELINE, key="investigate-in-timeline-action-item"
onClick: investigateInTimelineAlertClick, data-test-subj="investigate-in-timeline-action-item"
disabled: isFetchingAlertEcs, disabled={isFetchingAlertEcs === true}
}, onClick={investigateInTimelineAlertClick}
>
{ACTION_INVESTIGATE_IN_TIMELINE}
</EuiContextMenuItem>,
] ]
: []; : [];
return { return {
investigateInTimelineAction, investigateInTimelineActionItems,
investigateInTimelineAlertClick, investigateInTimelineAlertClick,
showInvestigateInTimelineAction, showInvestigateInTimelineAction,
}; };

View file

@ -185,13 +185,6 @@ export const ACTION_ADD_EVENT_FILTER = i18n.translate(
} }
); );
export const ACTION_ADD_TO_CASE = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.actions.addToCase',
{
defaultMessage: 'Add to case',
}
);
export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate( export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException', 'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException',
{ {

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import type { TimelineEventsDetailsItem } from '../../../../common'; import type { TimelineEventsDetailsItem } from '../../../../common';
import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils'; import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils';
import { HostStatus } from '../../../../common/endpoint/types'; import { HostStatus } from '../../../../common/endpoint/types';
@ -89,11 +90,14 @@ export const useHostIsolationAction = ({
isolationSupported && isolationSupported &&
isHostIsolationPanelOpen === false isHostIsolationPanelOpen === false
? [ ? [
{ <EuiContextMenuItem
name: isolateHostTitle, key="isolate-host-action-item"
onClick: isolateHostHandler, data-test-subj="isolate-host-action-item"
disabled: loadingHostIsolationStatus || agentStatus === HostStatus.UNENROLLED, disabled={loadingHostIsolationStatus || agentStatus === HostStatus.UNENROLLED}
}, onClick={isolateHostHandler}
>
{isolateHostTitle}
</EuiContextMenuItem>,
] ]
: [], : [],
[ [

View file

@ -1,18 +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 { ACTION_ADD_TO_CASE } from '../alerts_table/translations';
export const addToCaseActionItem = (timelineId: string | null | undefined) =>
['detections-page', 'detections-rules-details-page', 'timeline-1'].includes(timelineId ?? '')
? [
{
name: ACTION_ADD_TO_CASE,
panel: 2,
},
]
: [];

View file

@ -6,7 +6,7 @@
*/ */
import React, { useState, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { EuiContextMenu, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
@ -15,13 +15,10 @@ import { TimelineEventsDetailsItem, TimelineNonEcsData } from '../../../../commo
import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions';
import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions';
import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline'; import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline';
import { ACTION_ADD_TO_CASE } from '../alerts_table/translations';
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { useInsertTimeline } from '../../../cases/components/use_insert_timeline'; import { useInsertTimeline } from '../../../cases/components/use_insert_timeline';
import { addToCaseActionItem } from './helpers';
import { useEventFilterAction } from '../alerts_table/timeline_actions/use_event_filter_action'; import { useEventFilterAction } from '../alerts_table/timeline_actions/use_event_filter_action';
import { useHostIsolationAction } from '../host_isolation/use_host_isolation_action'; import { useHostIsolationAction } from '../host_isolation/use_host_isolation_action';
import { CHANGE_ALERT_STATUS } from './translations';
import { getFieldValue } from '../host_isolation/helpers'; import { getFieldValue } from '../host_isolation/helpers';
import type { Ecs } from '../../../../common/ecs'; import type { Ecs } from '../../../../common/ecs';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@ -121,7 +118,7 @@ export const TakeActionDropdown = React.memo(
[onAddIsolationStatusClick] [onAddIsolationStatusClick]
); );
const hostIsolationAction = useHostIsolationAction({ const hostIsolationActionItems = useHostIsolationAction({
closePopover: closePopoverHandler, closePopover: closePopoverHandler,
detailsData, detailsData,
onAddIsolationStatusClick: handleOnAddIsolationStatusClick, onAddIsolationStatusClick: handleOnAddIsolationStatusClick,
@ -136,7 +133,7 @@ export const TakeActionDropdown = React.memo(
[onAddExceptionTypeClick] [onAddExceptionTypeClick]
); );
const { exceptionActions } = useExceptionActions({ const { exceptionActionItems } = useExceptionActions({
isEndpointAlert, isEndpointAlert,
onAddExceptionTypeClick: handleOnAddExceptionTypeClick, onAddExceptionTypeClick: handleOnAddExceptionTypeClick,
}); });
@ -146,7 +143,7 @@ export const TakeActionDropdown = React.memo(
setIsPopoverOpen(false); setIsPopoverOpen(false);
}, [onAddEventFilterClick]); }, [onAddEventFilterClick]);
const eventFilterActions = useEventFilterAction({ const { eventFilterActionItems } = useEventFilterAction({
onAddEventFilterClick: handleOnAddEventFilterClick, onAddEventFilterClick: handleOnAddEventFilterClick,
}); });
@ -154,7 +151,7 @@ export const TakeActionDropdown = React.memo(
closePopoverHandler(); closePopoverHandler();
}, [closePopoverHandler]); }, [closePopoverHandler]);
const { actionItems } = useAlertsActions({ const { actionItems: statusActionItems } = useAlertsActions({
alertStatus: actionsData.alertStatus, alertStatus: actionsData.alertStatus,
eventId: actionsData.eventId, eventId: actionsData.eventId,
indexName, indexName,
@ -163,7 +160,7 @@ export const TakeActionDropdown = React.memo(
closePopover: closePopoverAndFlyout, closePopover: closePopoverAndFlyout,
}); });
const { investigateInTimelineAction } = useInvestigateInTimeline({ const { investigateInTimelineActionItems } = useInvestigateInTimeline({
alertIds, alertIds,
ecsRowData: ecsData, ecsRowData: ecsData,
onInvestigateInTimelineAlertClick: closePopoverHandler, onInvestigateInTimelineAlertClick: closePopoverHandler,
@ -172,15 +169,9 @@ export const TakeActionDropdown = React.memo(
const alertsActionItems = useMemo( const alertsActionItems = useMemo(
() => () =>
!isEvent && actionsData.ruleId !isEvent && actionsData.ruleId
? [ ? [...statusActionItems, ...exceptionActionItems]
{ : eventFilterActionItems,
name: CHANGE_ALERT_STATUS, [eventFilterActionItems, exceptionActionItems, statusActionItems, isEvent, actionsData.ruleId]
panel: 1,
},
...exceptionActions,
]
: [eventFilterActions],
[eventFilterActions, exceptionActions, isEvent, actionsData.ruleId]
); );
const addToCaseProps = useMemo(() => { const addToCaseProps = useMemo(() => {
@ -197,55 +188,35 @@ export const TakeActionDropdown = React.memo(
} }
}, [afterCaseSelection, casePermissions, ecsData, insertTimelineHook]); }, [afterCaseSelection, casePermissions, ecsData, insertTimelineHook]);
const panels = useMemo(() => { const addToCasesActionItems = useMemo(
if (tGridEnabled) { () =>
return [ addToCaseProps &&
{ ['detections-page', 'detections-rules-details-page', 'timeline-1'].includes(
id: 0, timelineId ?? ''
items: [ )
? [
timelinesUi.getAddToExistingCaseButton(addToCaseProps),
timelinesUi.getAddToNewCaseButton(addToCaseProps),
]
: [],
[timelinesUi, addToCaseProps, timelineId]
);
const items: React.ReactElement[] = useMemo(
() => [
...(tGridEnabled ? addToCasesActionItems : []),
...alertsActionItems, ...alertsActionItems,
...addToCaseActionItem(timelineId), ...hostIsolationActionItems,
...hostIsolationAction, ...investigateInTimelineActionItems,
...investigateInTimelineAction,
], ],
}, [
{
id: 1,
title: CHANGE_ALERT_STATUS,
content: <EuiContextMenuPanel size="s" items={actionItems} />,
},
{
id: 2,
title: ACTION_ADD_TO_CASE,
content: [
<>{addToCaseProps && timelinesUi.getAddToExistingCaseButton(addToCaseProps)}</>,
<>{addToCaseProps && timelinesUi.getAddToNewCaseButton(addToCaseProps)}</>,
],
},
];
} else {
return [
{
id: 0,
items: [...alertsActionItems, ...hostIsolationAction, ...investigateInTimelineAction],
},
{
id: 1,
title: CHANGE_ALERT_STATUS,
content: <EuiContextMenuPanel size="s" items={actionItems} />,
},
];
}
}, [
addToCaseProps,
alertsActionItems,
hostIsolationAction,
investigateInTimelineAction,
timelineId,
timelinesUi,
actionItems,
tGridEnabled, tGridEnabled,
]); alertsActionItems,
addToCasesActionItems,
hostIsolationActionItems,
investigateInTimelineActionItems,
]
);
const takeActionButton = useMemo(() => { const takeActionButton = useMemo(() => {
return ( return (
@ -255,10 +226,10 @@ export const TakeActionDropdown = React.memo(
); );
}, [togglePopoverHandler]); }, [togglePopoverHandler]);
return panels[0].items?.length && !loadingEventDetails ? ( return items.length && !loadingEventDetails ? (
<> <>
<EuiPopover <EuiPopover
id="hostIsolationTakeActionPanel" id="AlertTakeActionPanel"
button={takeActionButton} button={takeActionButton}
isOpen={isPopoverOpen} isOpen={isPopoverOpen}
closePopover={closePopoverHandler} closePopover={closePopoverHandler}
@ -266,7 +237,7 @@ export const TakeActionDropdown = React.memo(
anchorPosition="downLeft" anchorPosition="downLeft"
repositionOnScroll repositionOnScroll
> >
<EuiContextMenu size="s" initialPanelId={0} panels={panels} /> <EuiContextMenuPanel size="s" items={items} />
</EuiPopover> </EuiPopover>
</> </>
) : null; ) : null;

View file

@ -33,8 +33,9 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
<EuiContextMenuItem <EuiContextMenuItem
aria-label={ariaLabel} aria-label={ariaLabel}
data-test-subj="attach-alert-to-case-button" data-test-subj="attach-alert-to-case-button"
size="s"
onClick={addExistingCaseClick} onClick={addExistingCaseClick}
// needs forced size="s" since it is lazy loaded and the EuiContextMenuPanel can not initialize the size
size="s"
disabled={isDisabled} disabled={isDisabled}
> >
{i18n.ACTION_ADD_EXISTING_CASE} {i18n.ACTION_ADD_EXISTING_CASE}

View file

@ -34,8 +34,9 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
<EuiContextMenuItem <EuiContextMenuItem
aria-label={ariaLabel} aria-label={ariaLabel}
data-test-subj="attach-alert-to-case-button" data-test-subj="attach-alert-to-case-button"
size="s"
onClick={addNewCaseClick} onClick={addNewCaseClick}
// needs forced size="s" since it is lazy loaded and the EuiContextMenuPanel can not initialize the size
size="s"
disabled={isDisabled} disabled={isDisabled}
> >
{i18n.ACTION_ADD_NEW_CASE} {i18n.ACTION_ADD_NEW_CASE}

View file

@ -96,7 +96,7 @@ const BulkActionsComponent: React.FC<BulkActionsProps> = ({
<EuiPopover <EuiPopover
isOpen={isActionsPopoverOpen} isOpen={isActionsPopoverOpen}
anchorPosition="upCenter" anchorPosition="upCenter"
panelPaddingSize="s" panelPaddingSize="none"
button={ button={
<EuiButtonEmpty <EuiButtonEmpty
aria-label="selectedShowBulkActions" aria-label="selectedShowBulkActions"

View file

@ -29,7 +29,7 @@ export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
'xpack.timelines.timeline.closeSelectedTitle', 'xpack.timelines.timeline.closeSelectedTitle',
{ {
defaultMessage: 'Close selected', defaultMessage: 'Mark as closed',
} }
); );

View file

@ -127,7 +127,6 @@ export const useStatusBulkActionItems = ({
key="open" key="open"
data-test-subj="open-alert-status" data-test-subj="open-alert-status"
onClick={() => onClickUpdate(FILTER_OPEN)} onClick={() => onClickUpdate(FILTER_OPEN)}
size="s"
> >
{i18n.BULK_ACTION_OPEN_SELECTED} {i18n.BULK_ACTION_OPEN_SELECTED}
</EuiContextMenuItem> </EuiContextMenuItem>
@ -139,7 +138,6 @@ export const useStatusBulkActionItems = ({
key="acknowledge" key="acknowledge"
data-test-subj="acknowledged-alert-status" data-test-subj="acknowledged-alert-status"
onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED)} onClick={() => onClickUpdate(FILTER_ACKNOWLEDGED)}
size="s"
> >
{i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED} {i18n.BULK_ACTION_ACKNOWLEDGED_SELECTED}
</EuiContextMenuItem> </EuiContextMenuItem>
@ -151,7 +149,6 @@ export const useStatusBulkActionItems = ({
key="close" key="close"
data-test-subj="close-alert-status" data-test-subj="close-alert-status"
onClick={() => onClickUpdate(FILTER_CLOSED)} onClick={() => onClickUpdate(FILTER_CLOSED)}
size="s"
> >
{i18n.BULK_ACTION_CLOSE_SELECTED} {i18n.BULK_ACTION_CLOSE_SELECTED}
</EuiContextMenuItem> </EuiContextMenuItem>