Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Fix - Notes Flyout Product Feedback #188129

Merged
merged 9 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface NoteCardsProps {
eventId?: string;
timelineId: string;
onCancel?: () => void;
showToggleEventDetailsAction?: boolean;
}

/** A view for entering and reviewing notes */
Expand All @@ -65,6 +66,7 @@ export const NoteCards = React.memo<NoteCardsProps>(
eventId,
timelineId,
onCancel,
showToggleEventDetailsAction = true,
}) => {
const [newNote, setNewNote] = useState('');

Expand Down Expand Up @@ -109,7 +111,11 @@ export const NoteCards = React.memo<NoteCardsProps>(
<EuiScreenReaderOnly data-test-subj="screenReaderOnly">
<p>{i18n.YOU_ARE_VIEWING_NOTES(ariaRowindex)}</p>
</EuiScreenReaderOnly>
<NotePreviews timelineId={timelineId} notes={notes} />
<NotePreviews
timelineId={timelineId}
notes={notes}
showToggleEventDetailsAction={showToggleEventDetailsAction}
/>
</NotesContainer>
</NotePreviewsContainer>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,63 @@ describe('NotePreviews', () => {
expect(wrapper.find('[data-test-subj="delete-note"] button').prop('disabled')).toBeFalsy();
});

test('should render toggle event details action by default', () => {
const timeline = mockTimelineResults[0];
(useDeepEqualSelector as jest.Mock).mockReturnValue(timeline);

const wrapper = mountWithI18nProvider(
<TestProviders>
<NotePreviews
notes={[
{
noteId: 'noteId1',
note: 'enabled delete',
savedObjectId: 'test-id',
updated: note2updated,
updatedBy: 'alice',
},
]}
showTimelineDescription
timelineId="test-timeline-id"
/>
</TestProviders>,
{
wrappingComponent: createReactQueryWrapper(),
}
);

expect(wrapper.find('[data-test-subj="notes-toggle-event-details"]').exists()).toBeTruthy();
});

test('should not render toggle event details action when showToggleEventDetailsAction is false ', () => {
const timeline = mockTimelineResults[0];
(useDeepEqualSelector as jest.Mock).mockReturnValue(timeline);

const wrapper = mountWithI18nProvider(
<TestProviders>
<NotePreviews
notes={[
{
noteId: 'noteId1',
note: 'enabled delete',
savedObjectId: 'test-id',
updated: note2updated,
updatedBy: 'alice',
},
]}
showTimelineDescription
timelineId="test-timeline-id"
showToggleEventDetailsAction={false}
/>
</TestProviders>,
{
wrappingComponent: createReactQueryWrapper(),
}
);

expect(wrapper.find('[data-test-subj="notes-toggle-event-details"]').exists()).toBeFalsy();
});

describe('Delete Notes', () => {
it('should delete note correctly', async () => {
const timeline = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const ToggleEventDetailsButtonComponent: React.FC<ToggleEventDetailsButtonProps>

return (
<EuiButtonIcon
data-test-subj="notes-toggle-event-details"
title={i18n.TOGGLE_EXPAND_EVENT_DETAILS}
aria-label={i18n.TOGGLE_EXPAND_EVENT_DETAILS}
color="text"
Expand Down Expand Up @@ -204,10 +205,32 @@ const NoteActions = React.memo<{
savedObjectId?: string | null;
confirmingNoteId?: string | null;
eventIdToNoteIds?: Record<string, string[]>;
}>(({ eventId, timelineId, noteId, confirmingNoteId, eventIdToNoteIds, savedObjectId }) => {
return eventId && timelineId ? (
<>
<ToggleEventDetailsButton eventId={eventId} timelineId={timelineId} />
showToggleEventDetailsAction?: boolean;
}>(
({
eventId,
timelineId,
noteId,
confirmingNoteId,
eventIdToNoteIds,
savedObjectId,
showToggleEventDetailsAction = true,
}) => {
return eventId && timelineId ? (
<>
{showToggleEventDetailsAction ? (
<ToggleEventDetailsButton eventId={eventId} timelineId={timelineId} />
) : null}
<DeleteNoteButton
noteId={noteId}
eventId={eventId}
confirmingNoteId={confirmingNoteId}
savedObjectId={savedObjectId}
timelineId={timelineId}
eventIdToNoteIds={eventIdToNoteIds}
/>
</>
) : (
<DeleteNoteButton
noteId={noteId}
eventId={eventId}
Expand All @@ -216,18 +239,9 @@ const NoteActions = React.memo<{
timelineId={timelineId}
eventIdToNoteIds={eventIdToNoteIds}
/>
</>
) : (
<DeleteNoteButton
noteId={noteId}
eventId={eventId}
confirmingNoteId={confirmingNoteId}
savedObjectId={savedObjectId}
timelineId={timelineId}
eventIdToNoteIds={eventIdToNoteIds}
/>
);
});
);
}
);

NoteActions.displayName = 'NoteActions';
/**
Expand All @@ -238,10 +252,11 @@ interface NotePreviewsProps {
notes?: TimelineResultNote[] | null;
timelineId?: string;
showTimelineDescription?: boolean;
showToggleEventDetailsAction?: boolean;
}

export const NotePreviews = React.memo<NotePreviewsProps>(
({ notes, timelineId, showTimelineDescription }) => {
({ notes, timelineId, showTimelineDescription, showToggleEventDetailsAction = true }) => {
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []);
const timeline = useDeepEqualSelector((state) =>
Expand Down Expand Up @@ -315,6 +330,7 @@ export const NotePreviews = React.memo<NotePreviewsProps>(
savedObjectId={note.savedObjectId}
confirmingNoteId={timeline?.confirmingNoteId}
eventIdToNoteIds={eventIdToNoteIds}
showToggleEventDetailsAction={showToggleEventDetailsAction}
/>
),
timelineAvatar: (
Expand All @@ -326,7 +342,13 @@ export const NotePreviews = React.memo<NotePreviewsProps>(
),
};
}),
[eventIdToNoteIds, notes, timelineId, timeline?.confirmingNoteId]
[
eventIdToNoteIds,
notes,
timelineId,
timeline?.confirmingNoteId,
showToggleEventDetailsAction,
]
);

const commentList = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import React from 'react';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiFlyoutResizable,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
Expand All @@ -32,7 +32,7 @@ export type NotesFlyoutProps = {
* z-index override is needed because otherwise NotesFlyout appears below
* Timeline Modal as they both have same z-index of 1000
*/
const NotesFlyoutContainer = styled(EuiFlyout)`
const NotesFlyoutContainer = styled(EuiFlyoutResizable)`
/*
* We want the width of flyout to be less than 50% of screen because
* otherwise it interferes with the delete notes modal
Expand Down Expand Up @@ -61,7 +61,8 @@ export const NotesFlyout = React.memo(function NotesFlyout(props: NotesFlyoutPro
data-test-subj="timeline-notes-flyout"
onClose={onClose}
aria-labelledby={notesFlyoutTitleId}
maxWidth={750}
minWidth={500}
maxWidth={1400}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
Expand All @@ -80,6 +81,7 @@ export const NotesFlyout = React.memo(function NotesFlyout(props: NotesFlyoutPro
eventId={eventId}
timelineId={timelineId}
onCancel={onCancel}
showToggleEventDetailsAction={false}
/>
</EuiFlyoutBody>
</NotesFlyoutContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import React from 'react';
import { TimelineId } from '../../../../../common/types';
import { TimelineId, TimelineTabs } from '../../../../../common/types';
import { renderHook, act } from '@testing-library/react-hooks/dom';
import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock';
import type { UseNotesInFlyoutArgs } from './use_notes_in_flyout';
import { useNotesInFlyout } from './use_notes_in_flyout';
import { waitFor } from '@testing-library/react';
import { useDispatch } from 'react-redux';
Expand Down Expand Up @@ -85,11 +86,13 @@ const refetchMock = jest.fn();

const renderTestHook = () => {
return renderHook(
() =>
(props?: Partial<UseNotesInFlyoutArgs>) =>
useNotesInFlyout({
eventIdToNoteIds: mockEventIdToNoteIds,
timelineId: TimelineId.test,
refetch: refetchMock,
activeTab: TimelineTabs.query,
...props,
}),
{
wrapper: ({ children }) => (
Expand Down Expand Up @@ -198,4 +201,33 @@ describe('useNotesInFlyout', () => {

expect(result.current.isNotesFlyoutVisible).toBe(false);
});

it('should close the flyout when activeTab is changed', () => {
const { result, rerender, waitForNextUpdate } = renderTestHook();

act(() => {
result.current.setNotesEventId('event-1');
});

act(() => {
result.current.showNotesFlyout();
});

expect(result.current.isNotesFlyoutVisible).toBe(true);

act(() => {
// no change in active Tab
rerender({ activeTab: TimelineTabs.query });
});

expect(result.current.isNotesFlyoutVisible).toBe(true);

act(() => {
rerender({ activeTab: TimelineTabs.eql });
});

waitForNextUpdate();

expect(result.current.isNotesFlyoutVisible).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
* 2.0.
*/

import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import type { TimelineTabs } from '../../../../../common/types';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { appSelectors } from '../../../../common/store';
import { timelineActions } from '../../../store';

interface UseNotesInFlyoutArgs {
export interface UseNotesInFlyoutArgs {
eventIdToNoteIds: Record<string, string[]>;
refetch?: () => void;
timelineId: string;
activeTab: TimelineTabs;
}

const EMPTY_STRING_ARRAY: string[] = [];
Expand All @@ -36,12 +38,19 @@ export const useNotesInFlyout = (args: UseNotesInFlyoutArgs) => {
setIsNotesFlyoutVisible(true);
}, []);

const { eventIdToNoteIds, refetch, timelineId } = args;
const { eventIdToNoteIds, refetch, timelineId, activeTab } = args;

const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);

const notesById = useDeepEqualSelector(getNotesByIds);

useEffect(() => {
if (activeTab) {
// if activeTab changes, close the notes flyout
closeNotesFlyout();
}
}, [activeTab, closeNotesFlyout]);

const dispatch = useDispatch();

const noteIds: string[] = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
eventIdToNoteIds,
refetch,
timelineId,
activeTab: TimelineTabs.eql,
});

const onToggleShowNotes = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
eventIdToNoteIds,
refetch,
timelineId,
activeTab: TimelineTabs.pinned,
});

const onToggleShowNotes = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
eventIdToNoteIds,
refetch,
timelineId,
activeTab,
});

const onToggleShowNotes = useCallback(
Expand Down