[8.12] [Cases] Assignees Table Filter Does Not Reset Selected Options (#173021) (#173116)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[Cases] Assignees Table Filter Does Not Reset Selected Options
(#173021)](https://github.com/elastic/kibana/pull/173021)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Julian
Gernun","email":"17549662+jcger@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-12-11T20:58:33Z","message":"[Cases]
Assignees Table Filter Does Not Reset Selected Options
(#173021)\n\nCo-authored-by: Christos Nasikas
<christos.nasikas@elastic.co>","sha":"bc153bc65acd4f0814ada0c701380f162373fd14","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:ResponseOps","Feature:Cases","v8.12.0","v8.13.0"],"number":173021,"url":"https://github.com/elastic/kibana/pull/173021","mergeCommit":{"message":"[Cases]
Assignees Table Filter Does Not Reset Selected Options
(#173021)\n\nCo-authored-by: Christos Nasikas
<christos.nasikas@elastic.co>","sha":"bc153bc65acd4f0814ada0c701380f162373fd14"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173021","number":173021,"mergeCommit":{"message":"[Cases]
Assignees Table Filter Does Not Reset Selected Options
(#173021)\n\nCo-authored-by: Christos Nasikas
<christos.nasikas@elastic.co>","sha":"bc153bc65acd4f0814ada0c701380f162373fd14"}}]}]
BACKPORT-->

Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-12-12 05:28:21 -05:00 committed by GitHub
parent 837ecf80f5
commit 5f7be13c20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 171 additions and 119 deletions

View file

@ -156,7 +156,7 @@ export interface SystemFilterOptions {
severity: CaseSeverity[];
status: CaseStatuses[];
tags: string[];
assignees: Array<string | null> | null;
assignees: Array<string | null>;
reporters: User[];
owner: string[];
category: string[];

View file

@ -47,6 +47,7 @@ import { useLicense } from '../../common/use_license';
import * as api from '../../containers/api';
import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles';
jest.mock('../../containers/configure/use_get_case_configuration');
jest.mock('../../containers/use_get_cases');
@ -63,6 +64,7 @@ jest.mock('../app/use_available_owners', () => ({
}));
jest.mock('../../containers/use_update_case');
jest.mock('../../common/use_license');
jest.mock('../../containers/user_profiles/use_suggest_user_profiles');
const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock;
const useGetCasesMock = useGetCases as jest.Mock;
@ -74,6 +76,7 @@ const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
const useUpdateCaseMock = useUpdateCase as jest.Mock;
const useLicenseMock = useLicense as jest.Mock;
const useGetCategoriesMock = useGetCategories as jest.Mock;
const useSuggestUserProfilesMock = useSuggestUserProfiles as jest.Mock;
const mockTriggersActionsUiService = triggersActionsUiMock.createStart();
@ -164,6 +167,7 @@ describe('AllCasesListGeneric', () => {
useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap });
useUpdateCaseMock.mockReturnValue({ mutate: updateCaseProperty });
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => false });
useSuggestUserProfilesMock.mockReturnValue({ data: userProfiles, isLoading: false });
mockKibana();
moment.tz.setDefault('UTC');
window.localStorage.clear();
@ -1078,6 +1082,43 @@ describe('AllCasesListGeneric', () => {
).toBeGreaterThan(0);
});
});
it('should reset the assignees when deactivating the filter', async () => {
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
appMockRenderer.render(<AllCasesList />);
// Opens assignees filter and checks an option
const assigneesButton = screen.getByTestId('options-filter-popover-button-assignees');
userEvent.click(assigneesButton);
userEvent.click(screen.getByText('Damaged Raccoon'));
expect(within(assigneesButton).getByLabelText('1 active filters')).toBeInTheDocument();
// Deactivates assignees filter
userEvent.click(screen.getByRole('button', { name: 'More' }));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByRole('option', { name: 'Assignees' }));
expect(useGetCasesMock).toHaveBeenLastCalledWith({
filterOptions: {
...DEFAULT_FILTER_OPTIONS,
owner: [SECURITY_SOLUTION_OWNER],
assignees: [],
},
queryParams: DEFAULT_QUERY_PARAMS,
});
// Reopens assignees filter
userEvent.click(screen.getByRole('option', { name: 'Assignees' }));
// Opens the assignees popup
userEvent.click(assigneesButton);
expect(screen.getByLabelText('click to filter assignees')).toBeInTheDocument();
expect(
within(screen.getByTestId('options-filter-popover-button-assignees')).queryByLabelText(
'1 active filters'
)
).not.toBeInTheDocument();
});
});
});

View file

@ -51,19 +51,13 @@ describe('AssigneesFilterPopover', () => {
userEvent.click(screen.getByText('WD'));
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
"user": Object {
"email": "wet_dingo@elastic.co",
"full_name": "Wet Dingo",
"username": "wet_dingo",
},
},
]
`);
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
"u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
],
}
`);
});
it('calls onSelectionChange with a single user when different users are selected', async () => {
@ -83,32 +77,20 @@ describe('AssigneesFilterPopover', () => {
userEvent.click(screen.getByText('damaged_raccoon@elastic.co'));
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
"user": Object {
"email": "wet_dingo@elastic.co",
"full_name": "Wet Dingo",
"username": "wet_dingo",
},
},
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
"u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
],
}
`);
expect(onSelectionChange.mock.calls[1][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"user": Object {
"email": "damaged_raccoon@elastic.co",
"full_name": "Damaged Raccoon",
"username": "damaged_raccoon",
},
},
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
"u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
],
}
`);
});
@ -128,7 +110,7 @@ describe('AssigneesFilterPopover', () => {
it('shows the 1 assigned total when the users are passed in', async () => {
const props = {
...defaultProps,
selectedAssignees: [userProfiles[0]],
selectedAssignees: [userProfiles[0].uid],
};
appMockRender.render(<AssigneesFilterPopover {...props} />);
@ -145,7 +127,7 @@ describe('AssigneesFilterPopover', () => {
it('shows the total when the multiple users are selected', async () => {
const props = {
...defaultProps,
selectedAssignees: [userProfiles[0], userProfiles[1]],
selectedAssignees: [userProfiles[0].uid, userProfiles[1].uid],
};
appMockRender.render(<AssigneesFilterPopover {...props} />);
@ -239,9 +221,12 @@ describe('AssigneesFilterPopover', () => {
userEvent.click(screen.getByText('No assignees'));
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
null,
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
null,
],
}
`);
});
@ -261,39 +246,30 @@ describe('AssigneesFilterPopover', () => {
userEvent.click(screen.getByText('damaged_raccoon@elastic.co'));
expect(onSelectionChange.mock.calls[0][0]).toMatchInlineSnapshot(`
Array [
null,
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
null,
],
}
`);
expect(onSelectionChange.mock.calls[1][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
"user": Object {
"email": "wet_dingo@elastic.co",
"full_name": "Wet Dingo",
"username": "wet_dingo",
},
},
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
"u_9xDEQqUqoYCnFnPPLq5mIRHKL8gBTo_NiKgOnd5gGk0_0",
],
}
`);
expect(onSelectionChange.mock.calls[2][0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {},
"enabled": true,
"uid": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"user": Object {
"email": "damaged_raccoon@elastic.co",
"full_name": "Damaged Raccoon",
"username": "damaged_raccoon",
},
},
]
Object {
"filterId": "assignees",
"selectedOptionKeys": Array [
"u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
],
}
`);
});
@ -313,7 +289,7 @@ describe('AssigneesFilterPopover', () => {
});
it('shows warning message when reaching maximum limit to filter', async () => {
const maxAssignees = Array(MAX_ASSIGNEES_FILTER_LENGTH).fill(userProfiles[0]);
const maxAssignees = Array(MAX_ASSIGNEES_FILTER_LENGTH).fill(userProfiles[0].uid);
const props = {
...defaultProps,
selectedAssignees: maxAssignees,

View file

@ -6,6 +6,7 @@
*/
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { UserProfilesPopover } from '@kbn/user-profile-components';
import { isEmpty } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
@ -24,14 +25,17 @@ import { MAX_ASSIGNEES_FILTER_LENGTH } from '../../../common/constants';
export const NO_ASSIGNEES_VALUE = null;
export interface AssigneesFilterPopoverProps {
selectedAssignees: AssigneesFilteringSelection[];
selectedAssignees: Array<string | null>;
currentUserProfile: CurrentUserProfile;
isLoading: boolean;
onSelectionChange: (users: AssigneesFilteringSelection[]) => void;
onSelectionChange: (params: {
filterId: string;
selectedOptionKeys: Array<string | null>;
}) => void;
}
const AssigneesFilterPopoverComponent: React.FC<AssigneesFilterPopoverProps> = ({
selectedAssignees,
selectedAssignees: selectedAssigneesUids,
currentUserProfile,
isLoading,
onSelectionChange,
@ -48,8 +52,10 @@ const AssigneesFilterPopoverComponent: React.FC<AssigneesFilterPopoverProps> = (
const onChange = useCallback(
(users: AssigneesFilteringSelection[]) => {
const sortedUsers = orderAssigneesIncludingNone(currentUserProfile, users);
onSelectionChange(sortedUsers);
onSelectionChange({
filterId: 'assignees',
selectedOptionKeys: sortedUsers.map((user) => user?.uid ?? null),
});
},
[currentUserProfile, onSelectionChange]
);
@ -88,6 +94,16 @@ const AssigneesFilterPopoverComponent: React.FC<AssigneesFilterPopoverProps> = (
return sortedUsers;
}, [currentUserProfile, userProfiles, searchTerm]);
const selectedAssignees = selectedAssigneesUids
.map((uuid) => {
// this is the "no assignees" option
if (uuid === null) return null;
const userProfile = searchResultProfiles.find((user) => user?.uid === uuid);
return userProfile;
})
.filter(
(userProfile): userProfile is UserProfileWithAvatar | null => userProfile !== undefined
); // Filter out profiles that no longer exists
const isLoadingData = isLoading || isLoadingSuggest;
return (

View file

@ -27,7 +27,7 @@ const emptyFilterOptions: FilterOptions = {
severity: [],
status: [],
tags: [],
assignees: null,
assignees: [],
reporters: [],
owner: [],
category: [],

View file

@ -17,7 +17,6 @@ import * as i18n from '../translations';
import { SeverityFilter } from '../severity_filter';
import { AssigneesFilterPopover } from '../assignees_filter';
import type { CurrentUserProfile } from '../../types';
import type { AssigneesFilteringSelection } from '../../user_profiles/types';
import type { FilterChangeHandler, FilterConfig, FilterConfigRenderParams } from './types';
interface UseFilterConfigProps {
@ -28,13 +27,11 @@ interface UseFilterConfigProps {
countInProgressCases: number | null;
countOpenCases: number | null;
currentUserProfile: CurrentUserProfile;
handleSelectedAssignees: (newAssignees: AssigneesFilteringSelection[]) => void;
hiddenStatuses?: CaseStatuses[];
initialFilterOptions: Partial<FilterOptions>;
isLoading: boolean;
isSelectorView?: boolean;
onFilterOptionsChange: FilterChangeHandler;
selectedAssignees: AssigneesFilteringSelection[];
tags: string[];
}
@ -46,13 +43,11 @@ export const getSystemFilterConfig = ({
countInProgressCases,
countOpenCases,
currentUserProfile,
handleSelectedAssignees,
hiddenStatuses,
initialFilterOptions,
isLoading,
isSelectorView,
onFilterOptionsChange,
selectedAssignees,
tags,
}: UseFilterConfigProps): FilterConfig[] => {
const onSystemFilterChange = ({
@ -60,7 +55,7 @@ export const getSystemFilterConfig = ({
selectedOptionKeys,
}: {
filterId: string;
selectedOptionKeys: string[];
selectedOptionKeys: Array<string | null>;
}) => {
onFilterOptionsChange({
[filterId]: selectedOptionKeys,
@ -118,10 +113,10 @@ export const getSystemFilterConfig = ({
render: ({ filterOptions }: FilterConfigRenderParams) => {
return (
<AssigneesFilterPopover
selectedAssignees={selectedAssignees}
selectedAssignees={filterOptions?.assignees}
currentUserProfile={currentUserProfile}
isLoading={isLoading}
onSelectionChange={handleSelectedAssignees}
onSelectionChange={onSystemFilterChange}
/>
);
},
@ -199,13 +194,11 @@ export const useSystemFilterConfig = ({
countInProgressCases,
countOpenCases,
currentUserProfile,
handleSelectedAssignees,
hiddenStatuses,
initialFilterOptions,
isLoading,
isSelectorView,
onFilterOptionsChange,
selectedAssignees,
tags,
}: UseFilterConfigProps) => {
const filterConfig = getSystemFilterConfig({
@ -216,13 +209,11 @@ export const useSystemFilterConfig = ({
countInProgressCases,
countOpenCases,
currentUserProfile,
handleSelectedAssignees,
hiddenStatuses,
initialFilterOptions,
isLoading,
isSelectorView,
onFilterOptionsChange,
selectedAssignees,
tags,
});

View file

@ -165,6 +165,18 @@ describe('CasesTableFilters ', () => {
"assignees": Array [
"u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0",
],
"category": Array [],
"customFields": Object {},
"owner": Array [],
"reporters": Array [],
"search": "",
"searchFields": Array [
"title",
"description",
],
"severity": Array [],
"status": Array [],
"tags": Array [],
}
`);
});
@ -206,12 +218,11 @@ describe('CasesTableFilters ', () => {
it('should remove assignee from selected assignees when assignee no longer exists', async () => {
const overrideProps = {
...props,
initial: {
filterOptions: {
...DEFAULT_FILTER_OPTIONS,
assignees: [
// invalid profile uid
'123',
'u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0',
],
},
};
@ -233,6 +244,18 @@ describe('CasesTableFilters ', () => {
"assignees": Array [
"u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0",
],
"category": Array [],
"customFields": Object {},
"owner": Array [],
"reporters": Array [],
"search": "",
"searchFields": Array [
"title",
"description",
],
"severity": Array [],
"status": Array [],
"tags": Array [],
}
`);
});
@ -305,6 +328,32 @@ describe('CasesTableFilters ', () => {
expect(screen.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument();
});
it('shuld reset the assignees when deactivating the filter', async () => {
const overrideProps = {
...props,
filterOptions: {
...DEFAULT_FILTER_OPTIONS,
assignees: ['u_A_tM4n0wPkdiQ9smmd8o0Hr_h61XQfu8aRPh9GMoRoc_0'],
},
};
const license = licensingMock.createLicense({
license: { type: 'platinum' },
});
appMockRender = createAppMockRenderer({ license });
appMockRender.render(<CasesTableFilters {...overrideProps} />);
// deactivate the assignees filter
userEvent.click(screen.getByRole('button', { name: 'More' }));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByRole('option', { name: 'Assignees' }));
expect(onFilterChanged).toHaveBeenCalledWith({
...DEFAULT_FILTER_OPTIONS,
assignees: [],
});
});
});
describe('create case button', () => {

View file

@ -16,11 +16,10 @@ import { useGetTags } from '../../containers/use_get_tags';
import { useGetCategories } from '../../containers/use_get_categories';
import type { CurrentUserProfile } from '../types';
import { useCasesFeatures } from '../../common/use_cases_features';
import type { AssigneesFilteringSelection } from '../user_profiles/types';
import { useSystemFilterConfig } from './table_filter_config/use_system_filter_config';
import { useFilterConfig } from './table_filter_config/use_filter_config';
interface CasesTableFiltersProps {
export interface CasesTableFiltersProps {
countClosedCases: number | null;
countInProgressCases: number | null;
countOpenCases: number | null;
@ -56,23 +55,10 @@ const CasesTableFiltersComponent = ({
filterOptions,
}: CasesTableFiltersProps) => {
const [search, setSearch] = useState(filterOptions.search);
const [selectedAssignees, setSelectedAssignees] = useState<AssigneesFilteringSelection[]>([]);
const { data: tags = [] } = useGetTags();
const { data: categories = [] } = useGetCategories();
const { caseAssignmentAuthorized } = useCasesFeatures();
const handleSelectedAssignees = useCallback(
(newAssignees: AssigneesFilteringSelection[]) => {
if (!isEqual(newAssignees, selectedAssignees)) {
setSelectedAssignees(newAssignees);
onFilterChanged({
assignees: newAssignees.map((assignee) => assignee?.uid ?? null),
});
}
},
[selectedAssignees, onFilterChanged]
);
const onFilterOptionsChange = useCallback(
(partialFilterOptions: Partial<FilterOptions>) => {
const newFilterOptions = mergeWith({}, filterOptions, partialFilterOptions, mergeCustomizer);
@ -91,13 +77,11 @@ const CasesTableFiltersComponent = ({
countInProgressCases,
countOpenCases,
currentUserProfile,
handleSelectedAssignees,
hiddenStatuses,
initialFilterOptions,
isLoading,
isSelectorView,
onFilterOptionsChange,
selectedAssignees,
tags,
});

View file

@ -364,7 +364,7 @@ describe('Cases API', () => {
await getCases({
filterOptions: {
...DEFAULT_FILTER_OPTIONS,
assignees: null,
assignees: [],
},
queryParams: DEFAULT_QUERY_PARAMS,
signal: abortCtrl.signal,
@ -373,7 +373,7 @@ describe('Cases API', () => {
expect(fetchMock).toHaveBeenCalledWith(`${CASES_INTERNAL_URL}/_search`, {
method: 'POST',
body: JSON.stringify({
assignees: 'none',
assignees: undefined,
searchFields: DEFAULT_FILTER_OPTIONS.searchFields,
...DEFAULT_QUERY_PARAMS,
}),

View file

@ -160,10 +160,6 @@ describe('utils', () => {
expect(constructAssigneesFilter([])).toEqual({});
});
it('returns none if the assignees are null', () => {
expect(constructAssigneesFilter(null)).toEqual({ assignees: 'none' });
});
it('returns none for null values in the assignees array', () => {
expect(constructAssigneesFilter([null, '123'])).toEqual({ assignees: ['none', '123'] });
});

View file

@ -148,12 +148,11 @@ export const createUpdateSuccessToaster = (
export const constructAssigneesFilter = (
assignees: FilterOptions['assignees']
): { assignees?: string | string[] } =>
assignees === null || assignees.length > 0
assignees.length > 0
? {
assignees:
assignees?.map((assignee) =>
assignee === null ? NO_ASSIGNEES_FILTERING_KEYWORD : assignee
) ?? NO_ASSIGNEES_FILTERING_KEYWORD,
assignees: assignees?.map((assignee) =>
assignee === null ? NO_ASSIGNEES_FILTERING_KEYWORD : assignee
) ?? [NO_ASSIGNEES_FILTERING_KEYWORD],
}
: {};