Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections] Add assignees UI into alerts table (#7661) #167079

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9475776
[Security Solution][Detections] Add assignees UI into alerts table (#…
e40pud Sep 20, 2023
050200d
Tests
e40pud Sep 25, 2023
8a30b40
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 2, 2023
3a2e8ce
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 2, 2023
9b0d1b8
More tests
e40pud Oct 2, 2023
30485c7
More tests
e40pud Oct 2, 2023
c71d31b
More tests
e40pud Oct 3, 2023
2b916a9
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 3, 2023
e6af721
Unskip tests
e40pud Oct 3, 2023
1bd8b40
Fix tests
e40pud Oct 4, 2023
a5d4d77
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 4, 2023
94a5f5c
[Security Solution][Detections] Add assignees UI into alert's details…
e40pud Sep 21, 2023
637c496
Fix serverless tests
e40pud Oct 4, 2023
40791cd
Revert "[Security Solution][Detections] Add assignees UI into alert's…
e40pud Oct 4, 2023
42ffb01
Test
e40pud Oct 4, 2023
de39720
Fix tests
e40pud Oct 4, 2023
fa86f54
Fix tests
e40pud Oct 4, 2023
c6f0b2b
Updated APIs
e40pud Oct 5, 2023
479f184
Enable assignees search
e40pud Oct 5, 2023
1f031d7
Use UserProfileAPIClient to fetch user profiles by ids
e40pud Oct 6, 2023
65acc44
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 6, 2023
f9b8000
Fix tests
e40pud Oct 6, 2023
e56e499
Fix tests
e40pud Oct 6, 2023
09fe4cd
Fix tests
e40pud Oct 6, 2023
b705793
Fix cypress tests
e40pud Oct 6, 2023
da09246
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 6, 2023
67b0666
Merge branch 'security/feature/alert-user-assignment' into security/f…
e40pud Oct 9, 2023
d41c8bc
Show user profile avatars instead of names
e40pud Oct 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading