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

[ResponseOps][Cases] Filter by assignees #139441

Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0d40a95
Starting the filtering
jonathan-buttner Aug 24, 2022
171653b
Rough draft working for assignees filtering
jonathan-buttner Aug 25, 2022
4831041
Adding integration tests for new route
jonathan-buttner Aug 30, 2022
3f6b76f
Starting to write tests
jonathan-buttner Aug 30, 2022
27e5ea5
Fixing tests
jonathan-buttner Aug 31, 2022
eb9cb34
Merge branch 'cases-user-assignment' of github.com:elastic/kibana int…
jonathan-buttner Aug 31, 2022
00e27ab
Cleaning up more tests
jonathan-buttner Aug 31, 2022
4aeb721
Merge branch 'cases-user-assignment' of github.com:elastic/kibana int…
jonathan-buttner Aug 31, 2022
1e2fbeb
Removing duplicate call for current user
jonathan-buttner Aug 31, 2022
724d8b2
Fixing type errors and tests
jonathan-buttner Aug 31, 2022
483285f
Adding tests for filtering
jonathan-buttner Aug 31, 2022
cf9756c
Adding rbac tests
jonathan-buttner Aug 31, 2022
a8d84f7
Fixing translations
jonathan-buttner Aug 31, 2022
dc73702
Fixing api integration tests
jonathan-buttner Sep 1, 2022
9ce0c1c
Fixing severity tests
jonathan-buttner Sep 1, 2022
92a373e
Really fixing arrays equal
jonathan-buttner Sep 1, 2022
51415d1
Merge branch 'cases-user-assignment' of github.com:elastic/kibana int…
jonathan-buttner Sep 1, 2022
fef000a
Fixing ml tests and refactoring find assignees
jonathan-buttner Sep 1, 2022
8c7c3e8
Fixing cypress tests
jonathan-buttner Sep 1, 2022
11f410f
Fixing types
jonathan-buttner Sep 1, 2022
3f769e5
Fix tests
jonathan-buttner Sep 2, 2022
20b70e0
Merge branch 'cases-user-assignment' of github.com:elastic/kibana int…
jonathan-buttner Sep 2, 2022
1179f1a
Merge branch 'cases-user-assignment' of github.com:elastic/kibana int…
jonathan-buttner Sep 6, 2022
bf8bf54
Addressing first round of feedback
jonathan-buttner Sep 6, 2022
f880b7a
Reverting the recent cases changes
jonathan-buttner Sep 6, 2022
42e1b18
Fixing tests
jonathan-buttner Sep 6, 2022
55ac649
Fixing more tests and types
jonathan-buttner Sep 6, 2022
8fbe126
Allowing multi select
jonathan-buttner Sep 6, 2022
c798290
Fixing attachment framework issue
jonathan-buttner Sep 6, 2022
679241a
Addressing feedback
jonathan-buttner Sep 7, 2022
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
18 changes: 0 additions & 18 deletions x-pack/plugins/cases/common/api/cases/suggest_user_profiles.ts

This file was deleted.

5 changes: 5 additions & 0 deletions x-pack/plugins/cases/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,8 @@ export const PUSH_CASES_CAPABILITY = 'push_cases' as const;
*/

export const DEFAULT_USER_SIZE = 10;

/**
* Delays
*/
export const SEARCH_DEBOUNCE_MS = 500;
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export interface FilterOptions {
severity: CaseSeverityWithAll;
status: CaseStatusWithAllStatus;
tags: string[];
assignees: string[];
reporters: User[];
owner: string[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ import { registerConnectorsToMockActionRegistry } from '../../common/mock/regist
import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock';
import { waitForComponentToUpdate } from '../../common/test_utils';
import { useCreateAttachments } from '../../containers/use_create_attachments';
import { useGetReporters } from '../../containers/use_get_reporters';
import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics';
import { useGetConnectors } from '../../containers/configure/use_connectors';
import { useGetTags } from '../../containers/use_get_tags';
import { useUpdateCase } from '../../containers/use_update_case';
import { useGetCases } from '../../containers/use_get_cases';
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';

jest.mock('../../containers/use_create_attachments');
jest.mock('../../containers/use_bulk_update_case');
Expand All @@ -52,7 +54,8 @@ jest.mock('../../containers/use_get_cases_status');
jest.mock('../../containers/use_get_cases_metrics');
jest.mock('../../containers/use_get_action_license');
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/use_get_reporters');
jest.mock('../../containers/user_profiles/use_get_current_user_profile');
jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles');
jest.mock('../../containers/configure/use_connectors');
jest.mock('../../common/lib/kibana');
jest.mock('../../common/navigation/hooks');
Expand All @@ -67,7 +70,8 @@ const useGetCasesStatusMock = useGetCasesStatus as jest.Mock;
const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock;
const useUpdateCasesMock = useUpdateCases as jest.Mock;
const useGetTagsMock = useGetTags as jest.Mock;
const useGetReportersMock = useGetReporters as jest.Mock;
const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock;
const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock;
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
const useGetConnectorsMock = useGetConnectors as jest.Mock;
const useCreateAttachmentsMock = useCreateAttachments as jest.Mock;
Expand Down Expand Up @@ -145,6 +149,8 @@ describe('AllCasesListGeneric', () => {
handleIsLoading: jest.fn(),
isLoadingCases: [],
isSelectorView: false,
userProfiles: new Map(),
currentUserProfile: undefined,
};

let appMockRenderer: AppMockRenderer;
Expand All @@ -164,13 +170,8 @@ describe('AllCasesListGeneric', () => {
useGetCasesStatusMock.mockReturnValue(defaultCasesStatus);
useGetCasesMetricsMock.mockReturnValue(defaultCasesMetrics);
useGetTagsMock.mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() });
useGetReportersMock.mockReturnValue({
reporters: ['casetester'],
respReporters: [{ username: 'casetester' }],
isLoading: true,
isError: false,
fetchReporters: jest.fn(),
});
useGetCurrentUserProfileMock.mockReturnValue({ data: userProfiles[0], isLoading: false });
useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap });
useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false }));
useUpdateCaseMock.mockReturnValue({ updateCaseProperty });
mockKibana();
Expand All @@ -194,9 +195,9 @@ describe('AllCasesListGeneric', () => {
expect(
wrapper.find(`span[data-test-subj="case-table-column-tags-coke"]`).first().prop('title')
).toEqual(useGetCasesMockState.data.cases[0].tags[0]);
expect(wrapper.find(`[data-test-subj="case-table-column-createdBy"]`).first().text()).toEqual(
'LK'
);
expect(
wrapper.find(`[data-test-subj="case-user-profile-avatar-damaged_raccoon"]`).first().text()
).toEqual('DR');
expect(
wrapper
.find(`[data-test-subj="case-table-column-createdAt"]`)
Expand All @@ -215,20 +216,17 @@ describe('AllCasesListGeneric', () => {
});
});

it('should show a tooltip with the reporter username when hover over the reporter avatar', async () => {
it("should show a tooltip with the assignee's email when hover over the assignee avatar", async () => {
const result = render(
<TestProviders>
<AllCasesList />
</TestProviders>
);

userEvent.hover(result.queryAllByTestId('case-table-column-createdBy')[0]);
userEvent.hover(result.queryAllByTestId('case-user-profile-avatar-damaged_raccoon')[0]);

await waitFor(() => {
expect(result.getByTestId('case-table-column-createdBy-tooltip')).toBeTruthy();
expect(result.getByTestId('case-table-column-createdBy-tooltip').textContent).toEqual(
'lknope'
);
expect(result.getByText('[email protected]')).toBeInTheDocument();
});
});

Expand Down Expand Up @@ -263,6 +261,7 @@ describe('AllCasesListGeneric', () => {
title: null,
totalComment: null,
totalAlerts: null,
assignees: [],
},
],
},
Expand Down Expand Up @@ -588,7 +587,7 @@ describe('AllCasesListGeneric', () => {
wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click');
await waitFor(() => {
expect(onRowClick).toHaveBeenCalledWith({
assignees: [],
assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
closedAt: null,
closedBy: null,
comments: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
initialData,
useGetCases,
} from '../../containers/use_get_cases';
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';

const ProgressLoader = styled(EuiProgress)`
${({ $isShow }: { $isShow: boolean }) =>
Expand Down Expand Up @@ -88,6 +90,26 @@ export const AllCasesList = React.memo<AllCasesListProps>(
queryParams,
});

const assigneesFromCases = useMemo(() => {
return data.cases.reduce<Set<string>>((acc, caseInfo) => {
if (!caseInfo) {
return acc;
}

for (const assignee of caseInfo.assignees) {
acc.add(assignee.uid);
}
return acc;
}, new Set());
}, [data.cases]);

const { data: userProfiles } = useBulkGetUserProfiles({
jonathan-buttner marked this conversation as resolved.
Show resolved Hide resolved
uids: Array.from(assigneesFromCases),
});

const { data: currentUserProfile, isLoading: isLoadingCurrentUserProfile } =
useGetCurrentUserProfile();

const { data: connectors = [] } = useGetConnectors();

const sorting = useMemo(
Expand Down Expand Up @@ -193,6 +215,8 @@ export const AllCasesList = React.memo<AllCasesListProps>(

const columns = useCasesColumns({
filterStatus: filterOptions.status ?? StatusAll,
userProfiles: userProfiles ?? new Map(),
currentUserProfile,
handleIsLoading,
refreshCases,
isSelectorView,
Expand Down Expand Up @@ -245,6 +269,7 @@ export const AllCasesList = React.memo<AllCasesListProps>(
initial={{
search: filterOptions.search,
searchFields: filterOptions.searchFields,
assignees: filterOptions.assignees,
reporters: filterOptions.reporters,
tags: filterOptions.tags,
status: filterOptions.status,
Expand All @@ -255,6 +280,8 @@ export const AllCasesList = React.memo<AllCasesListProps>(
hiddenStatuses={hiddenStatuses}
displayCreateCaseButton={isSelectorView}
onCreateCasePressed={onRowClick}
isLoading={isLoadingCurrentUserProfile}
currentUserProfile={currentUserProfile}
/>
<CasesTable
columns={columns}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
* 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 React from 'react';
import userEvent from '@testing-library/user-event';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { AppMockRenderer, createAppMockRenderer } from '../../common/mock';
import { AssigneesFilterPopover, AssigneesFilterPopoverProps } from './assignees_filter';
import { userProfiles } from '../../containers/user_profiles/api.mock';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';

jest.mock('../../containers/user_profiles/api');

describe('AssigneesFilterPopover', () => {
let appMockRender: AppMockRenderer;
let defaultProps: AssigneesFilterPopoverProps;

beforeEach(() => {
jest.clearAllMocks();

appMockRender = createAppMockRenderer();

defaultProps = {
currentUserProfile: undefined,
selectedAssignees: [],
isLoading: false,
onSelectionChange: jest.fn(),
};
});

it('calls onSelectionChange when 1 user is selected', async () => {
const onSelectionChange = jest.fn();
const props = { ...defaultProps, onSelectionChange };
appMockRender.render(<AssigneesFilterPopover {...props} />);

await waitFor(() => {
userEvent.click(screen.getByTestId('options-filter-popover-button-assignees'));
expect(screen.getByPlaceholderText('Search users')).toBeInTheDocument();
});
await waitForEuiPopoverOpen();

fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
userEvent.click(screen.getByText('[email protected]'));

expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
"user": Object {
"email": "[email protected]",
"full_name": "Wet Dingo",
"username": "wet_dingo",
},
},
]
`);
});

it('calls onSelectionChange with a single user when different users are selected', async () => {
const onSelectionChange = jest.fn();
const props = { ...defaultProps, onSelectionChange };
appMockRender.render(<AssigneesFilterPopover {...props} />);

await waitFor(() => {
userEvent.click(screen.getByTestId('options-filter-popover-button-assignees'));
expect(screen.getByText('[email protected]'));
});

await waitForEuiPopoverOpen();

fireEvent.change(screen.getByPlaceholderText('Search users'), { target: { value: 'dingo' } });
userEvent.click(screen.getByText('[email protected]'));
userEvent.click(screen.getByText('[email protected]'));

expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
"user": Object {
"email": "[email protected]",
"full_name": "Wet Dingo",
"username": "wet_dingo",
},
},
]
`);
expect(onSelectionChange.mock.calls[1][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"user": Object {
"email": "[email protected]",
"full_name": "Damaged Raccoon",
"username": "damaged_raccoon",
},
},
]
`);
});

it('does not show the assigned users total if there are no assigned users', async () => {
appMockRender.render(<AssigneesFilterPopover {...defaultProps} />);

await waitFor(() => {
userEvent.click(screen.getByTestId('options-filter-popover-button-assignees'));
expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument();
});

await waitForEuiPopoverOpen();

expect(screen.queryByText('assignee')).not.toBeInTheDocument();
});

it('shows the 1 assigned total when the users are passed in', async () => {
const props = {
...defaultProps,
selectedAssignees: [userProfiles[0]],
};
appMockRender.render(<AssigneesFilterPopover {...props} />);

await waitFor(async () => {
userEvent.click(screen.getByTestId('options-filter-popover-button-assignees'));
expect(screen.getByText('1 assignee filtered')).toBeInTheDocument();
});

await waitForEuiPopoverOpen();

expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument();
});

it('shows three users when initially rendered', async () => {
appMockRender.render(<AssigneesFilterPopover {...defaultProps} />);

await waitFor(() => {
userEvent.click(screen.getByTestId('options-filter-popover-button-assignees'));
expect(screen.getByText('Wet Dingo')).toBeInTheDocument();
});
await waitForEuiPopoverOpen();

expect(screen.getByText('Damaged Raccoon')).toBeInTheDocument();
expect(screen.getByText('Physical Dinosaur')).toBeInTheDocument();
});
});
Loading