Skip to content

Commit

Permalink
[SecuritySolution] Update timeline actions (elastic#155692)
Browse files Browse the repository at this point in the history
## Summary

issue: elastic#155034 |
elastic#155586

Please find expected behaviours here:
elastic#155034 (comment)
  • Loading branch information
angorayc authored May 3, 2023
1 parent 264b0f6 commit 5be0f40
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,7 +51,7 @@ const defaultAddProviderAction = {
};

describe('createAddToNewTimelineCellAction', () => {
const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({
const addToTimelineCellActionFactory = createInvestigateInNewTimelineCellActionFactory({
store,
services,
});
Expand Down Expand Up @@ -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,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,23 @@

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';
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<string[]>((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,
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined>();
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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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();
});

Expand All @@ -195,7 +191,7 @@ describe('createAddToTimelineLensAction', () => {
...context,
data: [{ columnMeta }],
});
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith({
type: addProvider.type,
payload: {
Expand All @@ -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();
});

Expand All @@ -242,7 +232,7 @@ describe('createAddToTimelineLensAction', () => {
},
],
});
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith({
type: addProvider.type,
payload: {
Expand All @@ -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();
});

Expand All @@ -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');
});
});
Loading

0 comments on commit 5be0f40

Please sign in to comment.