Skip to content

Commit

Permalink
automatically pin an event when a note is added to it
Browse files Browse the repository at this point in the history
  • Loading branch information
janmonschke committed Apr 3, 2024
1 parent ba63ef2 commit 6a2b7a8
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,11 @@ const StatefulEventComponent: React.FC<Props> = ({

const activeTab = tabType ?? TimelineTabs.query;
const activeExpandedDetail = expandedDetail[activeTab];
const eventId = event._id;

const isDetailPanelExpanded: boolean =
(activeExpandedDetail?.panelView === 'eventDetail' &&
activeExpandedDetail?.params?.eventId === event._id) ||
activeExpandedDetail?.params?.eventId === eventId) ||
(activeExpandedDetail?.panelView === 'hostDetail' &&
activeExpandedDetail?.params?.hostName === hostName) ||
(activeExpandedDetail?.panelView === 'networkDetail' &&
Expand All @@ -161,7 +162,7 @@ const StatefulEventComponent: React.FC<Props> = ({

const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
const notesById = useDeepEqualSelector(getNotesByIds);
const noteIds: string[] = eventIdToNoteIds[event._id] || emptyNotes;
const noteIds: string[] = eventIdToNoteIds[eventId] || emptyNotes;

const notes: TimelineResultNote[] = useMemo(
() =>
Expand All @@ -181,8 +182,6 @@ const StatefulEventComponent: React.FC<Props> = ({
);

const onToggleShowNotes = useCallback(() => {
const eventId = event._id;

setShowNotes((prevShowNotes) => {
if (prevShowNotes[eventId]) {
// notes are closing, so focus the notes button on the next tick, after escaping the EuiFocusTrap
Expand All @@ -196,10 +195,9 @@ const StatefulEventComponent: React.FC<Props> = ({

return { ...prevShowNotes, [eventId]: !prevShowNotes[eventId] };
});
}, [event]);
}, [eventId]);

const handleOnEventDetailPanelOpened = useCallback(() => {
const eventId = event._id;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const indexName = event._index!;

Expand Down Expand Up @@ -235,7 +233,7 @@ const StatefulEventComponent: React.FC<Props> = ({
}
}, [
dispatch,
event._id,
eventId,
event._index,
expandableTimelineFlyoutEnabled,
isSecurityFlyoutEnabled,
Expand All @@ -249,14 +247,13 @@ const StatefulEventComponent: React.FC<Props> = ({
(noteId: string) => {
dispatch(
timelineActions.addNoteToEvent({
eventId: event._id,
eventId,
id: timelineId,
noteId,
pinEvent: !isEventPinned,
})
);
},
[dispatch, event, isEventPinned, timelineId]
[dispatch, eventId, timelineId]
);

const setEventsLoading = useCallback<SetEventsLoading>(
Expand Down Expand Up @@ -287,7 +284,7 @@ const StatefulEventComponent: React.FC<Props> = ({
showLeftBorder={!isEventViewer}
>
<EventColumnView
id={event._id}
id={eventId}
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={ariaRowindex}
columnHeaders={columnHeaders}
Expand All @@ -306,7 +303,7 @@ const StatefulEventComponent: React.FC<Props> = ({
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
showNotes={!!showNotes[event._id]}
showNotes={!!showNotes[eventId]}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={onToggleShowNotes}
Expand All @@ -327,7 +324,7 @@ const StatefulEventComponent: React.FC<Props> = ({
associateNote={associateNote}
data-test-subj="note-cards"
notes={notes}
showAddNote={!!showNotes[event._id]}
showAddNote={!!showNotes[eventId]}
toggleShowAddNote={onToggleShowNotes}
/>
</EventsTrSupplement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ describe('Body', () => {
eventId: '1',
id: 'timeline-test',
noteId: expect.anything(),
pinEvent: true,
},
type: timelineActions.addNoteToEvent({
eventId: '1',
Expand Down Expand Up @@ -401,7 +400,6 @@ describe('Body', () => {
eventId: '1',
id: 'timeline-test',
noteId: expect.anything(),
pinEvent: false,
},
type: timelineActions.addNoteToEvent({
eventId: '1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export const addNoteToEvent = actionCreator<{
id: string;
noteId: string;
eventId: string;
pinEvent?: boolean;
}>('ADD_NOTE_TO_EVENT');

export const deleteNoteFromEvent = actionCreator<{ id: string; noteId: string; eventId: string }>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { createMockStore, kibanaMock } from '../../../common/mock';
import { createMockStore, kibanaMock, mockGlobalState } from '../../../common/mock';
import { selectTimelineById } from '../selectors';
import { TimelineId } from '../../../../common/types/timeline';
import { persistNote } from '../../containers/notes/api';
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('Timeline note middleware', () => {
expect(saveTimelineMock).toHaveBeenCalled();
});

it('should pin the event when the `pinEvent` flag is passed', async () => {
it('should pin the event when the event is not pinned yet', async () => {
const testTimelineId = 'testTimelineId';
const testTimelineVersion = 'testVersion';
(persistNote as jest.Mock).mockResolvedValue({
Expand All @@ -178,7 +178,6 @@ describe('Timeline note middleware', () => {
eventId: testEventId,
id: TimelineId.test,
noteId: testNote.id,
pinEvent: true,
})
);

Expand All @@ -188,6 +187,53 @@ describe('Timeline note middleware', () => {
});
});

it('should not pin the event when the event is already pinned', async () => {
store = createMockStore(
{
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
pinnedEventIds: {
[testEventId]: true,
},
},
},
},
},
undefined,
kibanaMock
);
const testTimelineId = 'testTimelineId';
const testTimelineVersion = 'testVersion';
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
timelineId: testTimelineId,
timelineVersion: testTimelineVersion,
},
},
},
});

await store.dispatch(updateNote({ note: testNote }));
await store.dispatch(
addNoteToEvent({
eventId: testEventId,
id: TimelineId.test,
noteId: testNote.id,
})
);

expect(pinEventMock).not.toHaveBeenCalled();
});

it('should show an error message when the call is unauthorized', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{},
})
);

const currentTimeline = selectTimelineById(store.getState(), localTimelineId);

// In case a note was added to an unsaved timeline, we need to make sure to update the timeline
// locally and then remotely again in order not to lose the SO associations.
// This also involves setting the status and the default title.
if (!timeline.savedObjectId && response.note.timelineId && response.note.timelineVersion) {
const currentTimeline = selectTimelineById(store.getState(), localTimelineId);
await store.dispatch(
updateTimeline({
id: localTimelineId,
Expand All @@ -109,14 +110,17 @@ export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{},
await store.dispatch(saveTimeline({ id: localTimelineId, saveAsNew: false }));
}

// Events can be automatically pinned
if (isAddNoteToEventAction(action) && action.payload.pinEvent) {
await store.dispatch(
pinEvent({
id: localTimelineId,
eventId: action.payload.eventId,
})
);
// Automatically pin an associated event if it's not pinned yet
if (isAddNoteToEventAction(action)) {
const isEventPinned = currentTimeline.pinnedEventIds[action.payload.eventId] === true;
if (!isEventPinned) {
await store.dispatch(
pinEvent({
id: localTimelineId,
eventId: action.payload.eventId,
})
);
}
}
} catch (error) {
kibana.notifications.toasts.addDanger({
Expand Down

0 comments on commit 6a2b7a8

Please sign in to comment.