From 2d8e711d8a1cff0eaa220ce8d0a1b2df1a873b46 Mon Sep 17 00:00:00 2001 From: christineweng Date: Tue, 1 Oct 2024 16:28:17 -0500 Subject: [PATCH] fix note pinning --- .../components/header_actions/actions.tsx | 28 +++++++++++++------ .../public/notes/components/add_note.tsx | 19 ++++++++++++- .../public/notes/store/notes.slice.test.ts | 16 +++++++++++ .../public/notes/store/notes.slice.ts | 12 ++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index d6226ea0df00b..6db43ab7a2dc0 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -11,7 +11,10 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; import { TimelineTabs, TableId } from '@kbn/securitysolution-data-table'; -import { selectNotesByDocumentId } from '../../../notes/store/notes.slice'; +import { + selectNotesByDocumentId, + selectDocumentNotesBySavedObjectId, +} from '../../../notes/store/notes.slice'; import type { State } from '../../store'; import { selectTimelineById } from '../../../timelines/store/selectors'; import { @@ -70,7 +73,7 @@ const ActionsComponent: React.FC = ({ }) => { const dispatch = useDispatch(); - const { timelineType } = useShallowEqualSelector((state) => + const { timelineType, savedObjectId } = useShallowEqualSelector((state) => isTimelineScope(timelineId) ? selectTimelineById(state, timelineId) : timelineDefaults ); @@ -222,6 +225,12 @@ const ActionsComponent: React.FC = ({ /* only applicable for new event based notes */ const documentBasedNotes = useSelector((state: State) => selectNotesByDocumentId(state, eventId)); + const documentBasedNotesInTimeline = useSelector((state: State) => + selectDocumentNotesBySavedObjectId(state, { + documentId: eventId, + savedObjectId: savedObjectId ?? '', + }) + ); /* only applicable notes before event based notes */ const timelineNoteIds = useMemo( @@ -234,11 +243,14 @@ const ActionsComponent: React.FC = ({ [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled] ); - const noteIds = useMemo(() => { - return securitySolutionNotesEnabled - ? documentBasedNotes.map((note) => note.noteId) - : timelineNoteIds; - }, [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled]); + /* note ids specific to the current timeline, it is used to enable/disable pinning */ + const noteIdsInTimeline = useMemo(() => { + if (securitySolutionNotesEnabled) { + // if timeline is unsaved, there is no notes associated to timeline yet + return savedObjectId ? documentBasedNotesInTimeline.map((note) => note.noteId) : []; + } + return timelineNoteIds; + }, [documentBasedNotesInTimeline, timelineNoteIds, securitySolutionNotesEnabled, savedObjectId]); return ( @@ -291,7 +303,7 @@ const ActionsComponent: React.FC = ({ isAlert={isAlert(eventType)} key="pin-event" onPinClicked={handlePinClicked} - noteIds={noteIds} + noteIds={noteIdsInTimeline} eventIsPinned={isEventPinned} timelineType={timelineType} /> diff --git a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx index d54e0e42c86eb..f3811cd23af31 100644 --- a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx @@ -20,6 +20,9 @@ import { useKibana } from '../../common/lib/kibana'; import { ADD_NOTE_BUTTON_TEST_ID, ADD_NOTE_MARKDOWN_TEST_ID } from './test_ids'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; import type { State } from '../../common/store'; +import { timelineSelectors } from '../../timelines/store'; +import { TimelineId } from '../../../common/types'; +import { pinEvent } from '../../timelines/store/actions'; import { createNote, ReqStatus, @@ -77,6 +80,9 @@ export const AddNote = memo( const createStatus = useSelector((state: State) => selectCreateNoteStatus(state)); const createError = useSelector((state: State) => selectCreateNoteError(state)); + const activeTimeline = useSelector((state: State) => + timelineSelectors.selectTimelineById(state, TimelineId.active) + ); const addNote = useCallback(() => { dispatch( @@ -88,11 +94,22 @@ export const AddNote = memo( }, }) ); + + // Automatically pin an associated event if it's attached to a timeline and it's not pinned yet + const isEventPinned = eventId ? activeTimeline.pinnedEventIds[eventId] === true : false; + if (!isEventPinned && eventId && timelineId) { + dispatch( + pinEvent({ + id: TimelineId.active, + eventId, + }) + ); + } telemetry.reportAddNoteFromExpandableFlyoutClicked({ isRelatedToATimeline: timelineId != null, }); setEditorValue(''); - }, [dispatch, editorValue, eventId, telemetry, timelineId]); + }, [dispatch, editorValue, eventId, telemetry, timelineId, activeTimeline.pinnedEventIds]); // show a toast if the create note call fails useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts index 396940c892a6e..1c30fe9eda4be 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts @@ -27,6 +27,7 @@ import { selectNoteById, selectNoteIds, selectNotesByDocumentId, + selectDocumentNotesBySavedObjectId, selectNotesPagination, selectNotesTablePendingDeleteIds, selectNotesTableSearch, @@ -608,6 +609,21 @@ describe('notesSlice', () => { expect(selectNotesByDocumentId(mockGlobalState, 'wrong-document-id')).toHaveLength(0); }); + it('should return no notes if no notes is found with specified document id and saved object id', () => { + expect( + selectDocumentNotesBySavedObjectId(mockGlobalState, { + documentId: '1', + savedObjectId: 'wrong-savedObjectId', + }) + ).toHaveLength(0); + expect( + selectDocumentNotesBySavedObjectId(mockGlobalState, { + documentId: 'wrong-document-id', + savedObjectId: 'some-timeline-id', + }) + ).toHaveLength(0); + }); + it('should return all notes sorted for an existing document id', () => { const oldestNote = { eventId: '1', // should be a valid id based on mockTimelineData diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts index 3f0439e7298e4..6732f9491676e 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts @@ -318,6 +318,18 @@ export const selectNotesBySavedObjectId = createSelector( savedObjectId.length > 0 ? notes.filter((note) => note.timelineId === savedObjectId) : [] ); +export const selectDocumentNotesBySavedObjectId = createSelector( + [ + selectAllNotes, + ( + state: State, + { documentId, savedObjectId }: { documentId: string; savedObjectId: string } + ) => ({ documentId, savedObjectId }), + ], + (notes, { documentId, savedObjectId }) => + notes.filter((note) => note.eventId === documentId && note.timelineId === savedObjectId) +); + export const selectSortedNotesByDocumentId = createSelector( [ selectAllNotes,