forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Locked Status and Assignee Controls for Alert Page (elastic#7820)
- Loading branch information
Showing
7 changed files
with
352 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
...gins/security_solution/public/common/components/filter_group/filter_by_assignees.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* 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 { render } from '@testing-library/react'; | ||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; | ||
|
||
import { FilterByAssigneesPopover } from './filter_by_assignees'; | ||
import { TEST_IDS } from './constants'; | ||
import { TestProviders } from '../../mock'; | ||
|
||
const mockUserProfiles: UserProfileWithAvatar[] = [ | ||
{ | ||
uid: 'user-id-1', | ||
enabled: true, | ||
user: { username: 'user1', full_name: 'User 1', email: '[email protected]' }, | ||
data: {}, | ||
}, | ||
{ | ||
uid: 'user-id-2', | ||
enabled: true, | ||
user: { username: 'user2', full_name: 'User 2', email: '[email protected]' }, | ||
data: {}, | ||
}, | ||
{ | ||
uid: 'user-id-3', | ||
enabled: true, | ||
user: { username: 'user3', full_name: 'User 3', email: '[email protected]' }, | ||
data: {}, | ||
}, | ||
]; | ||
jest.mock('../../../detections/containers/detection_engine/alerts/use_suggest_users', () => { | ||
return { | ||
useSuggestUsers: () => ({ | ||
loading: false, | ||
userProfiles: mockUserProfiles, | ||
}), | ||
}; | ||
}); | ||
|
||
const renderFilterByAssigneesPopover = (alertAssignees: string[], onUsersChange = jest.fn()) => | ||
render( | ||
<TestProviders> | ||
<FilterByAssigneesPopover | ||
existingAssigneesIds={alertAssignees} | ||
onUsersChange={onUsersChange} | ||
/> | ||
</TestProviders> | ||
); | ||
|
||
describe('<FilterByAssigneesPopover />', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should render closed popover component', () => { | ||
const { getByTestId, queryByTestId } = renderFilterByAssigneesPopover([]); | ||
|
||
expect(getByTestId(TEST_IDS.FILTER_BY_ASSIGNEES_BUTTON)).toBeInTheDocument(); | ||
expect(queryByTestId('euiSelectableList')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should render opened popover component', () => { | ||
const { getByTestId } = renderFilterByAssigneesPopover([]); | ||
|
||
getByTestId(TEST_IDS.FILTER_BY_ASSIGNEES_BUTTON).click(); | ||
expect(getByTestId('euiSelectableList')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render assignees', () => { | ||
const { getByTestId } = renderFilterByAssigneesPopover([]); | ||
|
||
getByTestId(TEST_IDS.FILTER_BY_ASSIGNEES_BUTTON).click(); | ||
|
||
const assigneesList = getByTestId('euiSelectableList'); | ||
expect(assigneesList).toHaveTextContent('User 1'); | ||
expect(assigneesList).toHaveTextContent('[email protected]'); | ||
expect(assigneesList).toHaveTextContent('User 2'); | ||
expect(assigneesList).toHaveTextContent('[email protected]'); | ||
expect(assigneesList).toHaveTextContent('User 3'); | ||
expect(assigneesList).toHaveTextContent('[email protected]'); | ||
}); | ||
|
||
it('should call onUsersChange on clsing the popover', () => { | ||
const onUsersChangeMock = jest.fn(); | ||
const { getByTestId, getByText } = renderFilterByAssigneesPopover([], onUsersChangeMock); | ||
|
||
getByTestId(TEST_IDS.FILTER_BY_ASSIGNEES_BUTTON).click(); | ||
|
||
getByText('User 1').click(); | ||
getByText('User 2').click(); | ||
getByText('User 3').click(); | ||
getByText('User 3').click(); | ||
getByText('User 2').click(); | ||
getByText('User 1').click(); | ||
|
||
expect(onUsersChangeMock).toHaveBeenCalledTimes(6); | ||
expect(onUsersChangeMock.mock.calls).toEqual([ | ||
[['user-id-1']], | ||
[['user-id-2', 'user-id-1']], | ||
[['user-id-3', 'user-id-2', 'user-id-1']], | ||
[['user-id-2', 'user-id-1']], | ||
[['user-id-1']], | ||
[[]], | ||
]); | ||
}); | ||
}); |
117 changes: 117 additions & 0 deletions
117
...k/plugins/security_solution/public/common/components/filter_group/filter_by_assignees.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* | ||
* 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 { isEqual } from 'lodash/fp'; | ||
import type { FC } from 'react'; | ||
import React, { memo, useCallback, useEffect, useState } from 'react'; | ||
import { i18n } from '@kbn/i18n'; | ||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; | ||
import { UserProfilesPopover } from '@kbn/user-profile-components'; | ||
|
||
import { EuiFilterButton } from '@elastic/eui'; | ||
import { useSuggestUsers } from '../../../detections/containers/detection_engine/alerts/use_suggest_users'; | ||
import { TEST_IDS } from './constants'; | ||
|
||
export interface FilterByAssigneesPopoverProps { | ||
/** | ||
* Ids of the users assigned to the alert | ||
*/ | ||
existingAssigneesIds: string[]; | ||
|
||
/** | ||
* Callback to handle changing of the assignees selection | ||
*/ | ||
onUsersChange: (users: string[]) => void; | ||
} | ||
|
||
/** | ||
* The popover to filter alerts by assigned users | ||
*/ | ||
export const FilterByAssigneesPopover: FC<FilterByAssigneesPopoverProps> = memo( | ||
({ existingAssigneesIds, onUsersChange }) => { | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const { loading: isLoadingUsers, userProfiles } = useSuggestUsers(searchTerm); | ||
|
||
const [isPopoverOpen, setIsPopoverOpen] = useState(false); | ||
const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []); | ||
|
||
const [selectedAssignees, setSelectedAssignees] = useState<UserProfileWithAvatar[]>([]); | ||
useEffect(() => { | ||
if (isLoadingUsers) { | ||
return; | ||
} | ||
const assignees = userProfiles.filter((user) => existingAssigneesIds.includes(user.uid)); | ||
setSelectedAssignees(assignees); | ||
}, [existingAssigneesIds, isLoadingUsers, userProfiles]); | ||
|
||
const handleSelectedAssignees = useCallback( | ||
(newAssignees: UserProfileWithAvatar[]) => { | ||
if (!isEqual(newAssignees, selectedAssignees)) { | ||
setSelectedAssignees(newAssignees); | ||
onUsersChange(newAssignees.map((user) => user.uid)); | ||
} | ||
}, | ||
[onUsersChange, selectedAssignees] | ||
); | ||
|
||
const selectedStatusMessage = useCallback( | ||
(total: number) => | ||
i18n.translate( | ||
'xpack.securitySolution.flyout.right.visualizations.assignees.totalUsersAssigned', | ||
{ | ||
defaultMessage: '{total, plural, one {# filter} other {# filters}} selected', | ||
values: { total }, | ||
} | ||
), | ||
[] | ||
); | ||
|
||
return ( | ||
<UserProfilesPopover | ||
title={i18n.translate( | ||
'xpack.securitySolution.flyout.right.visualizations.assignees.popoverTitle', | ||
{ | ||
defaultMessage: 'Assignees', | ||
} | ||
)} | ||
button={ | ||
<EuiFilterButton | ||
data-test-subj={TEST_IDS.FILTER_BY_ASSIGNEES_BUTTON} | ||
iconType="arrowDown" | ||
onClick={togglePopover} | ||
isLoading={isLoadingUsers} | ||
isSelected={isPopoverOpen} | ||
hasActiveFilters={selectedAssignees.length > 0} | ||
numActiveFilters={selectedAssignees.length} | ||
> | ||
{i18n.translate('xpack.securitySolution.filtersGroup.assignees.buttonTitle', { | ||
defaultMessage: 'Assignees', | ||
})} | ||
</EuiFilterButton> | ||
} | ||
isOpen={isPopoverOpen} | ||
closePopover={togglePopover} | ||
panelStyle={{ | ||
minWidth: 520, | ||
}} | ||
selectableProps={{ | ||
onSearchChange: (term: string) => { | ||
setSearchTerm(term); | ||
}, | ||
onChange: handleSelectedAssignees, | ||
selectedStatusMessage, | ||
options: userProfiles, | ||
selectedOptions: selectedAssignees, | ||
isLoading: isLoadingUsers, | ||
height: 'full', | ||
}} | ||
/> | ||
); | ||
} | ||
); | ||
|
||
FilterByAssigneesPopover.displayName = 'FilterByAssigneesPopover'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.