Skip to content

Commit

Permalink
[Security Solution][Notes] - allow filtering by user (#195519)
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeOberti authored Oct 16, 2024
1 parent 7c38873 commit d85b51d
Show file tree
Hide file tree
Showing 21 changed files with 309 additions and 64 deletions.
5 changes: 5 additions & 0 deletions oas_docs/output/kibana.serverless.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35261,6 +35261,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
5 changes: 5 additions & 0 deletions oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35261,6 +35261,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
5 changes: 5 additions & 0 deletions oas_docs/output/kibana.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38692,6 +38692,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
5 changes: 5 additions & 0 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38692,6 +38692,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const GetNotesRequestQuery = z.object({
sortField: z.string().nullable().optional(),
sortOrder: z.string().nullable().optional(),
filter: z.string().nullable().optional(),
userFilter: z.string().nullable().optional(),
});
export type GetNotesRequestQueryInput = z.input<typeof GetNotesRequestQuery>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ paths:
schema:
type: string
nullable: true
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
description: Indicates the requested notes were returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ paths:
schema:
nullable: true
type: string
- in: query
name: userFilter
schema:
nullable: true
type: string
responses:
'200':
content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ export const mockGlobalState: State = {
direction: 'desc' as const,
},
filter: '',
userFilter: '',
search: '',
selectedIds: [],
pendingDeleteIds: [],
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/public/notes/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ export const fetchNotes = async ({
sortField,
sortOrder,
filter,
userFilter,
search,
}: {
page: number;
perPage: number;
sortField: string;
sortOrder: string;
filter: string;
userFilter: string;
search: string;
}) => {
const response = await KibanaServices.get().http.get<GetNotesResponse>(NOTE_URL, {
Expand All @@ -58,6 +60,7 @@ export const fetchNotes = async ({
sortField,
sortOrder,
filter,
userFilter,
search,
},
version: '2023-10-31',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const AddNote = memo(
createNote({
note: {
timelineId: timelineId || '',
eventId,
eventId: eventId || '',
note: editorValue,
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 userEvent from '@testing-library/user-event';
import React from 'react';
import { SearchRow } from './search_row';
import { SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids';
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';

jest.mock('../../common/components/user_profiles/use_suggest_users');

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');

return {
...original,
useDispatch: () => mockDispatch,
};
});

describe('SearchRow', () => {
beforeEach(() => {
jest.clearAllMocks();
(useSuggestUsers as jest.Mock).mockReturnValue({
isLoading: false,
data: [{ user: { username: 'test' } }, { user: { username: 'elastic' } }],
});
});

it('should render the component', () => {
const { getByTestId } = render(<SearchRow />);

expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument();
expect(getByTestId(USER_SELECT_TEST_ID)).toBeInTheDocument();
});

it('should call the correct action when entering a value in the search bar', async () => {
const { getByTestId } = render(<SearchRow />);

const searchBox = getByTestId(SEARCH_BAR_TEST_ID);

await userEvent.type(searchBox, 'test');
await userEvent.keyboard('{enter}');

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,27 @@
* 2.0.
*/

import { EuiFlexGroup, EuiFlexItem, EuiSearchBar } from '@elastic/eui';
import React, { useMemo, useCallback } from 'react';
import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiSearchBar } from '@elastic/eui';
import React, { useMemo, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { userSearchedNotes } from '..';
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 { SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids';
import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
import { userFilterUsers, userSearchedNotes } from '..';

const SearchRowContainer = styled.div`
&:not(:last-child) {
margin-bottom: ${(props) => props.theme.eui.euiSizeL};
}
`;

SearchRowContainer.displayName = 'SearchRowContainer';

const SearchRowFlexGroup = styled(EuiFlexGroup)`
margin-bottom: ${(props) => props.theme.eui.euiSizeXS};
`;

SearchRowFlexGroup.displayName = 'SearchRowFlexGroup';
export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', {
defaultMessage: 'Users',
});

export const SearchRow = React.memo(() => {
const dispatch = useDispatch();
const searchBox = useMemo(
() => ({
placeholder: 'Search note contents',
incremental: false,
'data-test-subj': 'notes-search-bar',
'data-test-subj': SEARCH_BAR_TEST_ID,
}),
[]
);
Expand All @@ -43,14 +37,43 @@ 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]
);

return (
<SearchRowContainer>
<SearchRowFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiSearchBar box={searchBox} onChange={onQueryChange} defaultQuery="" />
</EuiFlexItem>
</SearchRowFlexGroup>
</SearchRowContainer>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<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}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export const OPEN_FLYOUT_BUTTON_TEST_ID = `${PREFIX}OpenFlyoutButton` as const;
export const TIMELINE_DESCRIPTION_COMMENT_TEST_ID = `${PREFIX}TimelineDescriptionComment` as const;
export const NOTE_CONTENT_BUTTON_TEST_ID = `${PREFIX}NoteContentButton` as const;
export const NOTE_CONTENT_POPOVER_TEST_ID = `${PREFIX}NoteContentPopover` as const;
export const SEARCH_BAR_TEST_ID = `${PREFIX}SearchBar` as const;
export const USER_SELECT_TEST_ID = `${PREFIX}UserSelect` as const;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
selectNotesTableSelectedIds,
selectNotesTableSearch,
userSelectedBulkDelete,
selectNotesTableUserFilters,
} from '..';

export const BATCH_ACTIONS = i18n.translate(
Expand Down Expand Up @@ -51,6 +52,7 @@ export const NotesUtilityBar = React.memo(() => {
const pagination = useSelector(selectNotesPagination);
const sort = useSelector(selectNotesTableSort);
const selectedItems = useSelector(selectNotesTableSelectedIds);
const notesUserFilters = useSelector(selectNotesTableUserFilters);
const resultsCount = useMemo(() => {
const { perPage, page, total } = pagination;
const startOfCurrentPage = perPage * (page - 1) + 1;
Expand Down Expand Up @@ -83,10 +85,19 @@ export const NotesUtilityBar = React.memo(() => {
sortField: sort.field,
sortOrder: sort.direction,
filter: '',
userFilter: notesUserFilters,
search: notesSearch,
})
);
}, [dispatch, pagination.page, pagination.perPage, sort.field, sort.direction, notesSearch]);
}, [
dispatch,
pagination.page,
pagination.perPage,
sort.field,
sort.direction,
notesUserFilters,
notesSearch,
]);
return (
<UtilityBar border>
<UtilityBarSection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
selectNotesTablePendingDeleteIds,
selectFetchNotesError,
ReqStatus,
selectNotesTableUserFilters,
} from '..';
import type { NotesState } from '..';
import { SearchRow } from '../components/search_row';
Expand Down Expand Up @@ -119,6 +120,7 @@ export const NoteManagementPage = () => {
const pagination = useSelector(selectNotesPagination);
const sort = useSelector(selectNotesTableSort);
const notesSearch = useSelector(selectNotesTableSearch);
const notesUserFilters = useSelector(selectNotesTableUserFilters);
const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds);
const isDeleteModalVisible = pendingDeleteIds.length > 0;
const fetchNotesStatus = useSelector(selectFetchNotesStatus);
Expand All @@ -134,10 +136,19 @@ export const NoteManagementPage = () => {
sortField: sort.field,
sortOrder: sort.direction,
filter: '',
userFilter: notesUserFilters,
search: notesSearch,
})
);
}, [dispatch, pagination.page, pagination.perPage, sort.field, sort.direction, notesSearch]);
}, [
dispatch,
pagination.page,
pagination.perPage,
sort.field,
sort.direction,
notesUserFilters,
notesSearch,
]);

useEffect(() => {
fetchData();
Expand Down Expand Up @@ -212,6 +223,7 @@ export const NoteManagementPage = () => {
<Title title={i18n.NOTES} />
<EuiSpacer size="m" />
<SearchRow />
<EuiSpacer size="m" />
<NotesUtilityBar />
<EuiBasicTable
items={notes}
Expand Down
Loading

0 comments on commit d85b51d

Please sign in to comment.