From 27af6ef068ff3cb447cb3e2bc9fdc7d4a4b409e0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 25 Aug 2021 12:49:19 -0600 Subject: [PATCH] [Security Solution] Bugfix for disable state of External Alert context menu (#109914) --- .../timeline/body/actions/index.test.tsx | 203 ++++++++++-------- .../timeline/body/actions/index.tsx | 22 +- .../components/timeline/body/helpers.tsx | 2 - .../common/types/timeline/actions/index.ts | 28 +-- 4 files changed, 146 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index b982c2240ac7..cad6648cd1f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -10,16 +10,24 @@ import React from 'react'; import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../../common/mock'; import { Actions } from '.'; -import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; - -jest.mock('../../../../../common/hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; - -jest.mock('../../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +jest.mock('../../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false), })); +jest.mock('../../../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), +})); +jest.mock( + '../../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline', + () => ({ + useInvestigateInTimeline: jest.fn().mockReturnValue({ + investigateInTimelineActionItems: [], + investigateInTimelineAlertClick: jest.fn(), + showInvestigateInTimelineAction: false, + }), + }) +); jest.mock('@kbn/alerts', () => ({ useGetUserAlertsPermissions: () => ({ @@ -56,38 +64,35 @@ jest.mock('../../../../../common/lib/kibana', () => ({ useGetUserCasesPermissions: jest.fn(), })); -describe('Actions', () => { - beforeEach(() => { - (useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - }); +const defaultProps = { + ariaRowindex: 2, + checked: false, + columnId: '', + columnValues: 'abc def', + data: mockTimelineData[0].data, + ecsData: mockTimelineData[0].ecs, + eventId: 'abc', + eventIdToNoteIds: {}, + index: 2, + isEventPinned: false, + loadingEventIds: [], + onEventDetailsPanelOpened: () => {}, + onRowSelected: () => {}, + refetch: () => {}, + rowIndex: 10, + setEventsDeleted: () => {}, + setEventsLoading: () => {}, + showCheckboxes: true, + showNotes: false, + timelineId: 'test', + toggleShowNotes: () => {}, +}; +describe('Actions', () => { test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { const wrapper = mount( - + ); @@ -97,29 +102,7 @@ describe('Actions', () => { test('it does NOT render a checkbox for selecting the event when `showCheckboxes` is `false`', () => { const wrapper = mount( - + ); @@ -127,36 +110,88 @@ describe('Actions', () => { }); test('it does NOT render a checkbox for selecting the event when `tGridEnabled` is `true`', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); - + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); + describe('Alert context menu enabled?', () => { + test('it disables for eventType=raw', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(true); + }); + test('it enables for eventType=signal', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + signal: { rule: { id: ['123'] } }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(false); + }); + test('it disables for event.kind: undefined and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(true); + }); + test('it enables for event.kind: event and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['event'] }, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(false); + }); + test('it enables for event.kind: alert and agent.type: endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') + ).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index c14973f91d8c..73650bd320f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -15,8 +15,8 @@ import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use import { eventHasNotes, getEventType, getPinOnClick } from '../helpers'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; -import { AddEventNoteAction } from '../actions/add_note_icon_item'; -import { PinEventAction } from '../actions/pin_event_action'; +import { AddEventNoteAction } from './add_note_icon_item'; +import { PinEventAction } from './pin_event_action'; import { EventsTdContent } from '../../styles'; import * as i18n from '../translations'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; @@ -32,21 +32,20 @@ const ActionsContainer = styled.div` const ActionsComponent: React.FC = ({ ariaRowindex, - width, checked, columnValues, - eventId, data, ecsData, + eventId, eventIdToNoteIds, isEventPinned = false, isEventViewer = false, loadingEventIds, onEventDetailsPanelOpened, onRowSelected, + onRuleChange, refetch, showCheckboxes, - onRuleChange, showNotes, timelineId, toggleShowNotes, @@ -91,9 +90,14 @@ const ActionsComponent: React.FC = ({ ); const eventType = getEventType(ecsData); - const isEventContextMenuEnabledForEndpoint = useMemo( - () => ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint'), - [ecsData.event?.kind, ecsData.agent?.type] + const isContextMenuDisabled = useMemo( + () => + eventType !== 'signal' && + !( + (ecsData.event?.kind?.includes('event') || ecsData.event?.kind?.includes('alert')) && + ecsData.agent?.type?.includes('endpoint') + ), + [eventType, ecsData.event?.kind, ecsData.agent?.type] ); return ( @@ -163,7 +167,7 @@ const ActionsComponent: React.FC = ({ key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal' && !isEventContextMenuEnabledForEndpoint} + disabled={isContextMenuDisabled} refetch={refetch ?? noop} onRuleChange={onRuleChange} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 8ddea99cddaa..5b993110d38b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -125,6 +125,4 @@ export const getEventType = (event: Ecs): Omit => { export const ROW_RENDERER_CLASS_NAME = 'row-renderer'; -export const NOTES_CONTAINER_CLASS_NAME = 'notes-container'; - export const NOTE_CONTENT_CLASS_NAME = 'note-content'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index e8ba2718df69..281a1fcc9179 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -14,33 +14,33 @@ import { TimelineNonEcsData } from '../../../search_strategy'; import { Ecs } from '../../../ecs'; export interface ActionProps { - ariaRowindex: number; action?: RowCellRender; - width?: number; + ariaRowindex: number; + checked: boolean; columnId: string; columnValues: string; - checked: boolean; - disabled?: boolean; - onRowSelected: OnRowSelected; - eventId: string; - loadingEventIds: Readonly; - onEventDetailsPanelOpened: () => void; - showCheckboxes: boolean; data: TimelineNonEcsData[]; + disabled?: boolean; ecsData: Ecs; - index: number; + eventId: string; eventIdToNoteIds?: Readonly>; + index: number; isEventPinned?: boolean; isEventViewer?: boolean; - rowIndex: number; - setEventsLoading: SetEventsLoading; - setEventsDeleted: SetEventsDeleted; - refetch?: () => void; + loadingEventIds: Readonly; + onEventDetailsPanelOpened: () => void; + onRowSelected: OnRowSelected; onRuleChange?: () => void; + refetch?: () => void; + rowIndex: number; + setEventsDeleted: SetEventsDeleted; + setEventsLoading: SetEventsLoading; + showCheckboxes: boolean; showNotes?: boolean; tabType?: TimelineTabs; timelineId: string; toggleShowNotes?: () => void; + width?: number; } export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void;