Skip to content

Commit

Permalink
[Security Solution][Detections] Add assignees UI into alerts table (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
e40pud authored Oct 10, 2023
1 parent 19d081f commit dc9fcff
Show file tree
Hide file tree
Showing 49 changed files with 2,037 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './set_alert_assignees/set_alert_assignees_route';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './set_alert_assignees/set_alert_assignees_route.mock';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 type { SetAlertAssigneesRequestBody } from './set_alert_assignees_route';

export const getSetAlertAssigneesRequestMock = (
assigneesToAdd: string[] = [],
assigneesToRemove: string[] = [],
ids: string[] = []
): SetAlertAssigneesRequestBody => ({
assignees: { assignees_to_add: assigneesToAdd, assignees_to_remove: assigneesToRemove },
ids,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 * as t from 'io-ts';

import { alert_assignee_ids, alert_assignees } from '../../model';

export const setAlertAssigneesRequestBody = t.exact(
t.type({
assignees: alert_assignees,
ids: alert_assignee_ids,
})
);

export type SetAlertAssigneesRequestBody = t.TypeOf<typeof setAlertAssigneesRequestBody>;
export type SetAlertAssigneesRequestBodyDecoded = SetAlertAssigneesRequestBody;
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export const signal_status_query = t.object;
export const alert_tag_ids = t.array(t.string);
export type AlertTagIds = t.TypeOf<typeof alert_tag_ids>;

export const alert_assignee_ids = t.array(t.string);
export type AlertAssigneeIds = t.TypeOf<typeof alert_assignee_ids>;

export const fields = t.array(t.string);
export type Fields = t.TypeOf<typeof fields>;
export const fieldsOrUndefined = t.union([fields, t.undefined]);
Expand Down Expand Up @@ -135,3 +138,13 @@ export const alert_tags = t.type({
});

export type AlertTags = t.TypeOf<typeof alert_tags>;

export const alert_assignees = t.type({
assignees_to_add: t.array(t.string),
assignees_to_remove: t.array(t.string),
});

export type AlertAssignees = t.TypeOf<typeof alert_assignees>;

export const user_search_term = t.string;
export type UserSearchTerm = t.TypeOf<typeof user_search_term>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './suggest_user_profiles/suggest_user_profiles_route';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 * as t from 'io-ts';

import { user_search_term } from '../../model';

export const suggestUserProfilesRequestQuery = t.exact(
t.partial({
searchTerm: user_search_term,
})
);

export type SuggestUserProfilesRequestQuery = t.TypeOf<typeof suggestUserProfilesRequestQuery>;
export type SuggestUserProfilesRequestQueryDecoded = SuggestUserProfilesRequestQuery;
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ export const DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL =
export const DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL =
`${DETECTION_ENGINE_SIGNALS_URL}/finalize_migration` as const;
export const DETECTION_ENGINE_ALERT_TAGS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/tags` as const;
export const DETECTION_ENGINE_ALERT_ASSIGNEES_URL =
`${DETECTION_ENGINE_SIGNALS_URL}/assignees` as const;
export const DETECTION_ENGINE_ALERT_SUGGEST_USERS_URL =
`${DETECTION_ENGINE_SIGNALS_URL}/suggest_users` as const;

export const ALERTS_AS_DATA_URL = '/internal/rac/alerts' as const;
export const ALERTS_AS_DATA_FIND_URL = `${ALERTS_AS_DATA_URL}/find` as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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 type { TimelineItem } from '@kbn/timelines-plugin/common';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { act, fireEvent, render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../mock';
import { useSuggestUsers } from '../../../../detections/containers/detection_engine/alerts/use_suggest_users';

import { BulkAlertAssigneesPanel } from './alert_bulk_assignees';
import { ALERT_WORKFLOW_ASSIGNEE_IDS } from '@kbn/rule-data-utils';

jest.mock('../../../../detections/containers/detection_engine/alerts/use_suggest_users');

const mockUserProfiles: UserProfileWithAvatar[] = [
{ uid: 'default-test-assignee-id-1', enabled: true, user: { username: 'user1' }, data: {} },
{ uid: 'default-test-assignee-id-2', enabled: true, user: { username: 'user2' }, data: {} },
];

const mockAssigneeItems = [
{
_id: 'test-id',
data: [{ field: ALERT_WORKFLOW_ASSIGNEE_IDS, value: ['assignee-id-1', 'assignee-id-2'] }],
ecs: { _id: 'test-id' },
},
];

(useSuggestUsers as jest.Mock).mockReturnValue({ loading: false, userProfiles: mockUserProfiles });

const renderAssigneesMenu = (
items: TimelineItem[],
closePopover: () => void = jest.fn(),
onSubmit: () => Promise<void> = jest.fn(),
setIsLoading: () => void = jest.fn()
) => {
return render(
<TestProviders>
<BulkAlertAssigneesPanel
alertItems={items}
setIsLoading={setIsLoading}
closePopoverMenu={closePopover}
onSubmit={onSubmit}
/>
</TestProviders>
);
};

describe('BulkAlertAssigneesPanel', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('it renders', () => {
const wrapper = renderAssigneesMenu(mockAssigneeItems);

expect(wrapper.getByTestId('alert-assignees-update-button')).toBeInTheDocument();
expect(useSuggestUsers).toHaveBeenCalled();
});

test('it calls expected functions on submit when nothing has changed', () => {
const mockedClosePopover = jest.fn();
const mockedOnSubmit = jest.fn();
const mockedSetIsLoading = jest.fn();

const mockAssignees = [
{
_id: 'test-id',
data: [{ field: ALERT_WORKFLOW_ASSIGNEE_IDS, value: ['default-test-assignee-id-1'] }],
ecs: { _id: 'test-id' },
},
{
_id: 'test-id',
data: [
{
field: ALERT_WORKFLOW_ASSIGNEE_IDS,
value: ['default-test-assignee-id-1', 'default-test-assignee-id-2'],
},
],
ecs: { _id: 'test-id' },
},
];
const wrapper = renderAssigneesMenu(
mockAssignees,
mockedClosePopover,
mockedOnSubmit,
mockedSetIsLoading
);

act(() => {
fireEvent.click(wrapper.getByTestId('alert-assignees-update-button'));
});
expect(mockedClosePopover).toHaveBeenCalled();
expect(mockedOnSubmit).not.toHaveBeenCalled();
expect(mockedSetIsLoading).not.toHaveBeenCalled();
});

test('it updates state correctly', () => {
const mockAssignees = [
{
_id: 'test-id',
data: [{ field: ALERT_WORKFLOW_ASSIGNEE_IDS, value: ['default-test-assignee-id-1'] }],
ecs: { _id: 'test-id' },
},
{
_id: 'test-id',
data: [
{
field: ALERT_WORKFLOW_ASSIGNEE_IDS,
value: ['default-test-assignee-id-1', 'default-test-assignee-id-2'],
},
],
ecs: { _id: 'test-id' },
},
];
const wrapper = renderAssigneesMenu(mockAssignees);

expect(wrapper.getAllByRole('option')[0]).toHaveAttribute('title', 'user1');
expect(wrapper.getAllByRole('option')[0]).toBeChecked();
act(() => {
fireEvent.click(wrapper.getByText('user1'));
});
expect(wrapper.getAllByRole('option')[0]).toHaveAttribute('title', 'user1');
expect(wrapper.getAllByRole('option')[0]).not.toBeChecked();

expect(wrapper.getAllByRole('option')[1]).toHaveAttribute('title', 'user2');
expect(wrapper.getAllByRole('option')[1]).not.toBeChecked();
act(() => {
fireEvent.click(wrapper.getByText('user2'));
});
expect(wrapper.getAllByRole('option')[1]).toHaveAttribute('title', 'user2');
expect(wrapper.getAllByRole('option')[1]).toBeChecked();
});

test('it calls expected functions on submit when alerts have changed', () => {
const mockedClosePopover = jest.fn();
const mockedOnSubmit = jest.fn();
const mockedSetIsLoading = jest.fn();

const mockAssignees = [
{
_id: 'test-id',
data: [{ field: ALERT_WORKFLOW_ASSIGNEE_IDS, value: ['default-test-assignee-id-1'] }],
ecs: { _id: 'test-id' },
},
{
_id: 'test-id',
data: [
{
field: ALERT_WORKFLOW_ASSIGNEE_IDS,
value: ['default-test-assignee-id-1', 'default-test-assignee-id-2'],
},
],
ecs: { _id: 'test-id' },
},
];
const wrapper = renderAssigneesMenu(
mockAssignees,
mockedClosePopover,
mockedOnSubmit,
mockedSetIsLoading
);
act(() => {
fireEvent.click(wrapper.getByText('user1'));
});
act(() => {
fireEvent.click(wrapper.getByText('user2'));
});

act(() => {
fireEvent.click(wrapper.getByTestId('alert-assignees-update-button'));
});
expect(mockedClosePopover).toHaveBeenCalled();
expect(mockedOnSubmit).toHaveBeenCalled();
expect(mockedOnSubmit).toHaveBeenCalledWith(
{
assignees_to_add: ['default-test-assignee-id-2'],
assignees_to_remove: ['default-test-assignee-id-1'],
},
['test-id', 'test-id'],
expect.anything(), // An anonymous callback defined in the onSubmit function
mockedSetIsLoading
);
});
});
Loading

0 comments on commit dc9fcff

Please sign in to comment.