diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_charts.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_charts.cy.ts index d4efdeaada624..7799b55b55551 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_charts.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_charts.cy.ts @@ -22,8 +22,7 @@ import { GLOBAL_SEARCH_BAR_FILTER_ITEM, GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE, } from '../../screens/search_bar'; -import { TIMELINE_DATA_PROVIDERS_CONTAINER } from '../../screens/timeline'; -import { closeTimelineUsingCloseButton } from '../../tasks/security_main'; +import { TOASTER } from '../../screens/alerts_detection_rules'; describe('Histogram legend hover actions', { testIsolation: false }, () => { const ruleConfigs = getNewRule(); @@ -60,8 +59,7 @@ describe('Histogram legend hover actions', { testIsolation: false }, () => { it('Add To Timeline', function () { clickAlertsHistogramLegend(); clickAlertsHistogramLegendAddToTimeline(ruleConfigs.name); - cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should('be.visible'); - cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should('contain.text', getNewRule().name); - closeTimelineUsingCloseButton(); + + cy.get(TOASTER).should('have.text', `Added ${ruleConfigs.name} to timeline`); }); }); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts similarity index 67% rename from x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts rename to x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts index 7f3ed646da749..74b59b74ce5a5 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts @@ -6,10 +6,9 @@ */ import type { SecurityAppStore } from '../../../common/store/types'; -import type { DataProvider } from '../../../../common/types'; import { TimelineId } from '../../../../common/types'; -import { addProvider } from '../../../timelines/store/timeline/actions'; -import { createAddToNewTimelineCellActionFactory, getToastMessage } from './add_to_new_timeline'; +import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions'; +import { createInvestigateInNewTimelineCellActionFactory } from './investigate_in_new_timeline'; import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; @@ -52,7 +51,7 @@ const defaultAddProviderAction = { }; describe('createAddToNewTimelineCellAction', () => { - const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({ + const addToTimelineCellActionFactory = createInvestigateInNewTimelineCellActionFactory({ store, services, }); @@ -175,73 +174,22 @@ describe('createAddToNewTimelineCellAction', () => { }, }); }); - }); - - describe('getToastMessage', () => { - it('handles empty input', () => { - const result = getToastMessage({ queryMatch: { value: null } } as unknown as DataProvider); - expect(result).toEqual(''); - }); - it('handles array input', () => { - const result = getToastMessage({ - queryMatch: { value: ['hello', 'world'] }, - } as unknown as DataProvider); - expect(result).toEqual('hello, world alerts'); - }); - - it('handles single filter', () => { - const result = getToastMessage({ - queryMatch: { value }, - and: [{ queryMatch: { field: 'kibana.alert.severity', value: 'critical' } }], - } as unknown as DataProvider); - expect(result).toEqual(`critical severity alerts from ${value}`); - }); - - it('handles multiple filters', () => { - const result = getToastMessage({ - queryMatch: { value }, - and: [ - { - queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, - }, - { - queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, - }, - ], - } as unknown as DataProvider); - expect(result).toEqual(`open, critical severity alerts from ${value}`); - }); - - it('ignores unrelated filters', () => { - const result = getToastMessage({ - queryMatch: { value }, - and: [ - { - queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, - }, - { - queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, - }, - // currently only supporting the above fields - { - queryMatch: { field: 'user.name', value: 'something' }, - }, - ], - } as unknown as DataProvider); - expect(result).toEqual(`open, critical severity alerts from ${value}`); - }); - it('returns entity only when unrelated filters are passed', () => { - const result = getToastMessage({ - queryMatch: { value }, - and: [{ queryMatch: { field: 'user.name', value: 'something' } }], - } as unknown as DataProvider); - expect(result).toEqual(`${value} alerts`); - }); + it('should open the timeline', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + andFilters: [{ field: 'kibana.alert.severity', value: 'low' }], + }, + }); - it('returns entity only when no filters are passed', () => { - const result = getToastMessage({ queryMatch: { value }, and: [] } as unknown as DataProvider); - expect(result).toEqual(`${value} alerts`); + expect(mockDispatch).toBeCalledWith({ + type: showTimeline.type, + payload: { + id: TimelineId.active, + show: true, + }, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts similarity index 67% rename from x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts rename to x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts index 886162803bbf1..e1be2a374a6b3 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts @@ -7,19 +7,15 @@ import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions'; import { timelineActions } from '../../../timelines/store/timeline'; -import { addProvider } from '../../../timelines/store/timeline/actions'; -import type { DataProvider } from '../../../../common/types'; +import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions'; import { TimelineId } from '../../../../common/types'; import type { SecurityAppStore } from '../../../common/store'; import { fieldHasCellActions } from '../../utils'; import { - ADD_TO_NEW_TIMELINE, ADD_TO_TIMELINE_FAILED_TEXT, ADD_TO_TIMELINE_FAILED_TITLE, ADD_TO_TIMELINE_ICON, - ADD_TO_TIMELINE_SUCCESS_TITLE, - ALERTS_COUNT, - SEVERITY, + INVESTIGATE_IN_TIMELINE, } from '../constants'; import { createDataProviders, isValidDataProviderField } from '../data_provider'; import { SecurityCellActionType } from '../../constants'; @@ -27,32 +23,7 @@ import type { StartServices } from '../../../types'; import type { SecurityCellAction } from '../../types'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -const severityField = 'kibana.alert.severity'; -const statusField = 'kibana.alert.workflow_status'; - -export const getToastMessage = ({ queryMatch: { value }, and = [] }: DataProvider) => { - if (value == null) { - return ''; - } - const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString(); - - const descriptors = and.reduce((msg, { queryMatch }) => { - if (Array.isArray(queryMatch.value)) { - return msg; - } - if (queryMatch.field === severityField) { - msg.push(SEVERITY(queryMatch.value.toString())); - } - if (queryMatch.field === statusField) { - msg.push(queryMatch.value.toString()); - } - return msg; - }, []); - - return ALERTS_COUNT(fieldValue, descriptors.join(', ')); -}; - -export const createAddToNewTimelineCellActionFactory = createCellActionFactory( +export const createInvestigateInNewTimelineCellActionFactory = createCellActionFactory( ({ store, services, @@ -63,10 +34,10 @@ export const createAddToNewTimelineCellActionFactory = createCellActionFactory( const { notifications: notificationsService } = services; return { - type: SecurityCellActionType.ADD_TO_TIMELINE, + type: SecurityCellActionType.INVESTIGATE_IN_NEW_TIMELINE, getIconType: () => ADD_TO_TIMELINE_ICON, - getDisplayName: () => ADD_TO_NEW_TIMELINE, - getDisplayNameTooltip: () => ADD_TO_NEW_TIMELINE, + getDisplayName: () => INVESTIGATE_IN_TIMELINE, + getDisplayNameTooltip: () => INVESTIGATE_IN_TIMELINE, isCompatible: async ({ field }) => fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type), execute: async ({ field, metadata }) => { @@ -101,10 +72,9 @@ export const createAddToNewTimelineCellActionFactory = createCellActionFactory( id: TimelineId.active, }) ); + store.dispatch(showTimeline({ id: TimelineId.active, show: true })); + store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders })); - notificationsService.toasts.addSuccess({ - title: ADD_TO_TIMELINE_SUCCESS_TITLE(getToastMessage(dataProviders[0])), - }); } else { notificationsService.toasts.addWarning({ title: ADD_TO_TIMELINE_FAILED_TITLE, diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts index 0396cad110367..a4b37a6d2fc49 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts @@ -15,7 +15,7 @@ export const ADD_TO_TIMELINE = i18n.translate( defaultMessage: 'Add to timeline', } ); -export const ADD_TO_NEW_TIMELINE = i18n.translate( +export const INVESTIGATE_IN_TIMELINE = i18n.translate( 'xpack.securitySolution.actions.cellValue.addToNewTimeline.displayName', { defaultMessage: 'Investigate in timeline', diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts index 72e6eee17e4d4..5aeaa99800829 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts @@ -6,5 +6,5 @@ */ export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline'; -export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline'; +export { createInvestigateInNewTimelineCellActionFactory } from './cell_action/investigate_in_new_timeline'; export { createAddToTimelineLensAction } from './lens/add_to_timeline'; diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts index 9f172d4e18b3e..ebb1f40061a72 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.test.ts @@ -4,25 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { Subject } from 'rxjs'; import type { CellValueContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { LENS_EMBEDDABLE_TYPE } from '@kbn/lens-plugin/public'; import type { SecurityAppStore } from '../../../common/store/types'; -import { createAddToTimelineLensAction } from './add_to_timeline'; +import { createAddToTimelineLensAction, getInvestigatedValue } from './add_to_timeline'; import { KibanaServices } from '../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../common/constants'; -import { Subject } from 'rxjs'; -import { TimelineId } from '../../../../common/types'; -import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions'; +import type { DataProvider } from '../../../../common/types'; +import { TimelineId, EXISTS_OPERATOR } from '../../../../common/types'; +import { addProvider } from '../../../timelines/store/timeline/actions'; import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; jest.mock('../../../common/lib/kibana'); const currentAppId$ = new Subject(); KibanaServices.get().application.currentAppId$ = currentAppId$.asObservable(); const mockWarningToast = jest.fn(); +const mockSuccessToast = jest.fn(); KibanaServices.get().notifications.toasts.addWarning = mockWarningToast; - +KibanaServices.get().notifications.toasts.addSuccess = mockSuccessToast; const mockDispatch = jest.fn(); const store = { dispatch: mockDispatch, @@ -158,7 +159,7 @@ describe('createAddToTimelineLensAction', () => { describe('execute', () => { it('should execute normally', async () => { await addToTimelineAction.execute(context); - expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith({ type: addProvider.type, payload: { @@ -180,13 +181,8 @@ describe('createAddToTimelineLensAction', () => { ], }, }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: showTimeline.type, - payload: { - id: TimelineId.active, - show: true, - }, - }); + + expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockWarningToast).not.toHaveBeenCalled(); }); @@ -195,7 +191,7 @@ describe('createAddToTimelineLensAction', () => { ...context, data: [{ columnMeta }], }); - expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith({ type: addProvider.type, payload: { @@ -217,13 +213,7 @@ describe('createAddToTimelineLensAction', () => { ], }, }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: showTimeline.type, - payload: { - id: TimelineId.active, - show: true, - }, - }); + expect(mockSuccessToast).toHaveBeenCalledTimes(1); expect(mockWarningToast).not.toHaveBeenCalled(); }); @@ -242,7 +232,7 @@ describe('createAddToTimelineLensAction', () => { }, ], }); - expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith({ type: addProvider.type, payload: { @@ -264,13 +254,8 @@ describe('createAddToTimelineLensAction', () => { ], }, }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: showTimeline.type, - payload: { - id: TimelineId.active, - show: true, - }, - }); + + expect(mockSuccessToast).toHaveBeenCalledTimes(1); expect(mockWarningToast).not.toHaveBeenCalled(); }); @@ -293,3 +278,28 @@ describe('createAddToTimelineLensAction', () => { }); }); }); + +describe('getInvestigatedValue', () => { + it('handles empty input', () => { + const result = getInvestigatedValue([ + { queryMatch: { value: null } }, + ] as unknown as DataProvider[]); + expect(result).toEqual(''); + }); + it('handles array input', () => { + const result = getInvestigatedValue([ + { + queryMatch: { value: ['hello', 'world'] }, + }, + ] as unknown as DataProvider[]); + expect(result).toEqual('hello, world'); + }); + it('handles number value', () => { + const result = getInvestigatedValue([ + { + queryMatch: { value: '', operator: EXISTS_OPERATOR, field: 'host.name' }, + }, + ] as unknown as DataProvider[]); + expect(result).toEqual('host.name'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.ts index 4efe1d2a0122a..4032a2fe1fe8f 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/lens/add_to_timeline.ts @@ -10,15 +10,16 @@ import { isErrorEmbeddable, isFilterableEmbeddable } from '@kbn/embeddable-plugi import { createAction } from '@kbn/ui-actions-plugin/public'; import { KibanaServices } from '../../../common/lib/kibana'; import type { SecurityAppStore } from '../../../common/store/types'; -import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions'; +import { addProvider } from '../../../timelines/store/timeline/actions'; import type { DataProvider } from '../../../../common/types'; -import { TimelineId } from '../../../../common/types'; +import { EXISTS_OPERATOR, TimelineId } from '../../../../common/types'; import { fieldHasCellActions, isInSecurityApp, isLensEmbeddable } from '../../utils'; import { ADD_TO_TIMELINE, ADD_TO_TIMELINE_FAILED_TEXT, ADD_TO_TIMELINE_FAILED_TITLE, ADD_TO_TIMELINE_ICON, + ADD_TO_TIMELINE_SUCCESS_TITLE, } from '../constants'; import { createDataProviders } from '../data_provider'; @@ -38,6 +39,26 @@ function isDataColumnsFilterable(data?: CellValueContext['data']): boolean { ); } +export const getInvestigatedValue = (dataProviders: DataProvider[]) => { + const dataValue = dataProviders.reduce( + (acc, { queryMatch: { value, operator, field } }) => { + if (value != null) { + // This is the case when value is a number, and queried by fieldName: * + if (operator === EXISTS_OPERATOR) { + acc.push(field); + } else { + const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString(); + acc.push(fieldValue); + } + } + + return acc; + }, + [] + ); + return dataValue.join(', '); +}; + export const createAddToTimelineLensAction = ({ store, order, @@ -84,7 +105,13 @@ export const createAddToTimelineLensAction = ({ if (dataProviders.length > 0) { store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders })); - store.dispatch(showTimeline({ id: TimelineId.active, show: true })); + + const investigatedValue = getInvestigatedValue(dataProviders); + if (investigatedValue.length > 0) { + toastsService.addSuccess({ + title: ADD_TO_TIMELINE_SUCCESS_TITLE(investigatedValue), + }); + } } else { toastsService.addWarning({ title: ADD_TO_TIMELINE_FAILED_TITLE, diff --git a/x-pack/plugins/security_solution/public/actions/constants.ts b/x-pack/plugins/security_solution/public/actions/constants.ts index eff71171fbcea..ad8b3da853396 100644 --- a/x-pack/plugins/security_solution/public/actions/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/constants.ts @@ -14,6 +14,7 @@ export enum SecurityCellActionType { FILTER = 'security-cellAction-type-filter', COPY = 'security-cellAction-type-copyToClipboard', ADD_TO_TIMELINE = 'security-cellAction-type-addToTimeline', + INVESTIGATE_IN_NEW_TIMELINE = 'security-cellAction-type-investigateInNewTimeline', SHOW_TOP_N = 'security-cellAction-type-showTopN', TOGGLE_COLUMN = 'security-cellAction-type-toggleColumn', } diff --git a/x-pack/plugins/security_solution/public/actions/register.ts b/x-pack/plugins/security_solution/public/actions/register.ts index bfea8d27163c4..9b6453c9dc8d0 100644 --- a/x-pack/plugins/security_solution/public/actions/register.ts +++ b/x-pack/plugins/security_solution/public/actions/register.ts @@ -13,7 +13,7 @@ import { createFilterInCellActionFactory, createFilterOutCellActionFactory } fro import { createAddToTimelineLensAction, createAddToTimelineCellActionFactory, - createAddToNewTimelineCellActionFactory, + createInvestigateInNewTimelineCellActionFactory, } from './add_to_timeline'; import { createShowTopNCellActionFactory } from './show_top_n'; import { @@ -53,7 +53,7 @@ const registerCellActions = ( filterIn: createFilterInCellActionFactory({ store, services }), filterOut: createFilterOutCellActionFactory({ store, services }), addToTimeline: createAddToTimelineCellActionFactory({ store, services }), - addToNewTimeline: createAddToNewTimelineCellActionFactory({ store, services }), + investigateInNewTimeline: createInvestigateInNewTimelineCellActionFactory({ store, services }), showTopN: createShowTopNCellActionFactory({ store, history, services }), copyToClipboard: createCopyToClipboardCellActionFactory({ services }), toggleColumn: createToggleColumnCellActionFactory({ store }), @@ -83,7 +83,7 @@ const registerCellActions = ( registerCellActionsTrigger({ triggerId: SecurityCellActionsTrigger.ALERTS_COUNT, cellActions, - actionsOrder: ['addToNewTimeline'], + actionsOrder: ['investigateInNewTimeline'], services, }); }; diff --git a/x-pack/plugins/security_solution/public/actions/types.ts b/x-pack/plugins/security_solution/public/actions/types.ts index e0cd6e764f3b8..d1e25987205b8 100644 --- a/x-pack/plugins/security_solution/public/actions/types.ts +++ b/x-pack/plugins/security_solution/public/actions/types.ts @@ -57,7 +57,7 @@ export interface SecurityCellActions { filterIn?: CellActionFactory; filterOut?: CellActionFactory; addToTimeline?: CellActionFactory; - addToNewTimeline?: CellActionFactory; + investigateInNewTimeline?: CellActionFactory; showTopN?: CellActionFactory; copyToClipboard?: CellActionFactory; toggleColumn?: CellActionFactory;