mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[8.16] [Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149) (#197244)
# Backport This will backport the following commits from `main` to `8.16`: - [[Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149)](https://github.com/elastic/kibana/pull/197149) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Philippe Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2024-10-22T13:32:48Z","message":"[Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149)","sha":"dcd8e0c614183ae648e00979eb82123656076d16","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["backport","release_note:skip","v9.0.0","Team:Threat Hunting:Investigations","v8.16.0"],"title":"[Security Solution][Notes] - fix user filter not checking correct license in notes management page","number":197149,"url":"https://github.com/elastic/kibana/pull/197149","mergeCommit":{"message":"[Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149)","sha":"dcd8e0c614183ae648e00979eb82123656076d16"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197149","number":197149,"mergeCommit":{"message":"[Security Solution][Notes] - fix user filter not checking correct license in notes management page (#197149)","sha":"dcd8e0c614183ae648e00979eb82123656076d16"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
parent
bbe0c0602a
commit
0b47f783e9
8 changed files with 202 additions and 61 deletions
|
@ -46,3 +46,11 @@ export const ALERT_SUPPRESSION_RULE_DETAILS = i18n.translate(
|
|||
'Alert suppression is configured but will not be applied due to insufficient licensing',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) =>
|
||||
i18n.translate('securitySolutionPackages.noteManagement.userFilter.upsell', {
|
||||
defaultMessage: 'Upgrade to {requiredLicense} to make use of user filters',
|
||||
values: {
|
||||
requiredLicense,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -27,4 +27,5 @@ export type UpsellingMessageId =
|
|||
| 'investigation_guide_interactions'
|
||||
| 'alert_assignments'
|
||||
| 'alert_suppression_rule_form'
|
||||
| 'alert_suppression_rule_details';
|
||||
| 'alert_suppression_rule_details'
|
||||
| 'note_management_user_filter';
|
||||
|
|
|
@ -13,10 +13,6 @@ import { suggestUsers } from './api';
|
|||
import { USER_PROFILES_FAILURE } from './translations';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
|
||||
export interface SuggestUserProfilesArgs {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export const bulkGetUserProfiles = async ({
|
||||
searchTerm,
|
||||
}: {
|
||||
|
@ -25,7 +21,21 @@ export const bulkGetUserProfiles = async ({
|
|||
return suggestUsers({ searchTerm });
|
||||
};
|
||||
|
||||
export const useSuggestUsers = ({ searchTerm }: { searchTerm: string }) => {
|
||||
export interface UseSuggestUsersParams {
|
||||
/**
|
||||
* Search term to filter user profiles
|
||||
*/
|
||||
searchTerm: string;
|
||||
/**
|
||||
* Whether the query should be enabled
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches user profiles based on a search term
|
||||
*/
|
||||
export const useSuggestUsers = ({ enabled = true, searchTerm }: UseSuggestUsersParams) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
return useQuery<UserProfileWithAvatar[]>(
|
||||
|
@ -36,6 +46,7 @@ export const useSuggestUsers = ({ searchTerm }: { searchTerm: string }) => {
|
|||
{
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
enabled,
|
||||
onError: (e) => {
|
||||
addError(e, { title: USER_PROFILES_FAILURE });
|
||||
},
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { SearchRow } from './search_row';
|
||||
import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids';
|
||||
import { AssociatedFilter } from '../../../common/notes/constants';
|
||||
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
||||
jest.mock('../../common/components/user_profiles/use_suggest_users');
|
||||
|
||||
|
@ -35,7 +36,11 @@ describe('SearchRow', () => {
|
|||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
const { getByTestId } = render(<SearchRow />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SearchRow />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(USER_SELECT_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -43,7 +48,11 @@ describe('SearchRow', () => {
|
|||
});
|
||||
|
||||
it('should call the correct action when entering a value in the search bar', async () => {
|
||||
const { getByTestId } = render(<SearchRow />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SearchRow />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const searchBox = getByTestId(SEARCH_BAR_TEST_ID);
|
||||
|
||||
|
@ -53,20 +62,12 @@ describe('SearchRow', () => {
|
|||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the correct action when select a user', async () => {
|
||||
const { getByTestId } = render(<SearchRow />);
|
||||
|
||||
const userSelect = getByTestId('comboBoxSearchInput');
|
||||
userSelect.focus();
|
||||
|
||||
const option = await screen.findByText('test');
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the correct action when select a value in the associated note dropdown', async () => {
|
||||
const { getByTestId } = render(<SearchRow />);
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SearchRow />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const associatedNoteSelect = getByTestId(ASSOCIATED_NOT_SELECT_TEST_ID);
|
||||
await userEvent.selectOptions(associatedNoteSelect, [AssociatedFilter.documentOnly]);
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { EuiSelectOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSearchBar,
|
||||
|
@ -16,17 +15,12 @@ import {
|
|||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
|
||||
import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids';
|
||||
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
|
||||
import { userFilterAssociatedNotes, userFilterUsers, userSearchedNotes } from '..';
|
||||
import { UserFilterDropdown } from './user_filter_dropdown';
|
||||
import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID } from './test_ids';
|
||||
import { userFilterAssociatedNotes, userSearchedNotes } from '..';
|
||||
import { AssociatedFilter } from '../../../common/notes/constants';
|
||||
|
||||
export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', {
|
||||
defaultMessage: 'Users',
|
||||
});
|
||||
const FILTER_SELECT = i18n.translate('xpack.securitySolution.notes.management.filterSelect', {
|
||||
defaultMessage: 'Select filter',
|
||||
});
|
||||
|
@ -55,26 +49,6 @@ export const SearchRow = React.memo(() => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingSuggestedUsers, data: userProfiles } = useSuggestUsers({
|
||||
searchTerm: '',
|
||||
});
|
||||
const users = useMemo(
|
||||
() =>
|
||||
(userProfiles || []).map((userProfile: UserProfileWithAvatar) => ({
|
||||
label: userProfile.user.full_name || userProfile.user.username,
|
||||
})),
|
||||
[userProfiles]
|
||||
);
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<Array<EuiComboBoxOptionOption<string>>>();
|
||||
const onChange = useCallback(
|
||||
(user: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedUser(user);
|
||||
dispatch(userFilterUsers(user.length > 0 ? user[0].label : ''));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onAssociatedNoteSelectChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
dispatch(userFilterAssociatedNotes(e.target.value as AssociatedFilter));
|
||||
|
@ -88,15 +62,7 @@ export const SearchRow = React.memo(() => {
|
|||
<EuiSearchBar box={searchBox} onChange={onQueryChange} defaultQuery="" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiComboBox
|
||||
prepend={USERS_DROPDOWN}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={users}
|
||||
selectedOptions={selectedUser}
|
||||
onChange={onChange}
|
||||
isLoading={isLoadingSuggestedUsers}
|
||||
data-test-subj={USER_SELECT_TEST_ID}
|
||||
/>
|
||||
<UserFilterDropdown />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { UserFilterDropdown } from './user_filter_dropdown';
|
||||
import { USER_SELECT_TEST_ID } from './test_ids';
|
||||
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
|
||||
import { useLicense } from '../../common/hooks/use_license';
|
||||
import { useUpsellingMessage } from '../../common/hooks/use_upselling';
|
||||
|
||||
jest.mock('../../common/components/user_profiles/use_suggest_users');
|
||||
jest.mock('../../common/hooks/use_license');
|
||||
jest.mock('../../common/hooks/use_upselling');
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('UserFilterDropdown', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useSuggestUsers as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: [{ user: { username: 'test' } }, { user: { username: 'elastic' } }],
|
||||
});
|
||||
(useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => true });
|
||||
(useUpsellingMessage as jest.Mock).mockReturnValue('upsellingMessage');
|
||||
});
|
||||
|
||||
it('should render the component enabled', () => {
|
||||
const { getByTestId } = render(<UserFilterDropdown />);
|
||||
|
||||
const dropdown = getByTestId(USER_SELECT_TEST_ID);
|
||||
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(dropdown).not.toHaveClass('euiComboBox-isDisabled');
|
||||
});
|
||||
|
||||
it('should render the dropdown disabled', async () => {
|
||||
(useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => false });
|
||||
|
||||
const { getByTestId } = render(<UserFilterDropdown />);
|
||||
|
||||
expect(getByTestId(USER_SELECT_TEST_ID)).toHaveClass('euiComboBox-isDisabled');
|
||||
});
|
||||
|
||||
it('should call the correct action when select a user', async () => {
|
||||
const { getByTestId } = render(<UserFilterDropdown />);
|
||||
|
||||
const userSelect = getByTestId('comboBoxSearchInput');
|
||||
userSelect.focus();
|
||||
|
||||
const option = await screen.findByText('test');
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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, { useMemo, useCallback, useState } from 'react';
|
||||
import { EuiComboBox, EuiToolTip } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
|
||||
import { useLicense } from '../../common/hooks/use_license';
|
||||
import { useUpsellingMessage } from '../../common/hooks/use_upselling';
|
||||
import { USER_SELECT_TEST_ID } from './test_ids';
|
||||
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
|
||||
import { userFilterUsers } from '..';
|
||||
|
||||
export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', {
|
||||
defaultMessage: 'Users',
|
||||
});
|
||||
|
||||
export const UserFilterDropdown = React.memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const upsellingMessage = useUpsellingMessage('note_management_user_filter');
|
||||
|
||||
const { isLoading, data } = useSuggestUsers({
|
||||
searchTerm: '',
|
||||
enabled: isPlatinumPlus,
|
||||
});
|
||||
const users = useMemo(
|
||||
() =>
|
||||
(data || []).map((userProfile: UserProfileWithAvatar) => ({
|
||||
label: userProfile.user.full_name || userProfile.user.username,
|
||||
})),
|
||||
[data]
|
||||
);
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<Array<EuiComboBoxOptionOption<string>>>();
|
||||
const onChange = useCallback(
|
||||
(user: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedUser(user);
|
||||
dispatch(userFilterUsers(user.length > 0 ? user[0].label : ''));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const dropdown = useMemo(
|
||||
() => (
|
||||
<EuiComboBox
|
||||
prepend={USERS_DROPDOWN}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={users}
|
||||
selectedOptions={selectedUser}
|
||||
onChange={onChange}
|
||||
isLoading={isPlatinumPlus && isLoading}
|
||||
isDisabled={!isPlatinumPlus}
|
||||
data-test-subj={USER_SELECT_TEST_ID}
|
||||
/>
|
||||
),
|
||||
[isLoading, isPlatinumPlus, onChange, selectedUser, users]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPlatinumPlus ? (
|
||||
<>{dropdown}</>
|
||||
) : (
|
||||
<EuiToolTip position="bottom" content={upsellingMessage}>
|
||||
{dropdown}
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
UserFilterDropdown.displayName = 'UserFilterDropdown';
|
|
@ -12,6 +12,7 @@ import {
|
|||
ALERT_SUPPRESSION_RULE_FORM,
|
||||
UPGRADE_ALERT_ASSIGNMENTS,
|
||||
UPGRADE_INVESTIGATION_GUIDE,
|
||||
UPGRADE_NOTES_MANAGEMENT_USER_FILTER,
|
||||
} from '@kbn/security-solution-upselling/messages';
|
||||
import type {
|
||||
MessageUpsellings,
|
||||
|
@ -132,4 +133,9 @@ export const upsellingMessages: UpsellingMessages = [
|
|||
minimumLicenseRequired: 'platinum',
|
||||
message: ALERT_SUPPRESSION_RULE_DETAILS,
|
||||
},
|
||||
{
|
||||
id: 'note_management_user_filter',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
message: UPGRADE_NOTES_MANAGEMENT_USER_FILTER('Platinum'),
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue