Skip to content
6 changes: 0 additions & 6 deletions packages/kbn-babel-preset/styled_components_files.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const DeleteConfirmModal = React.memo(({ refetch = true }: { refetch?: bo
return (
<EuiConfirmModal
aria-labelledby={'delete-notes-modal'}
data-test-subj={'delete-notes-modal'}
title={DELETE}
onCancel={onCancel}
onConfirm={onConfirm}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface DeleteNoteButtonIconProps {
/**
* Renders a button to delete a note.
* This button works in combination with the DeleteConfirmModal.
* There should be a DeleteConfirmModal component as an ancestor of this component with visibility based on the redux state.
*/
export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconProps) => {
const dispatch = useDispatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ import { EuiAvatar, EuiComment, EuiCommentList, EuiLoadingElastic } from '@elast
import { useSelector } from 'react-redux';
import { FormattedRelative } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { DeleteConfirmModal } from './delete_confirm_modal';
import { OpenFlyoutButtonIcon } from './open_flyout_button';
import { OpenTimelineButtonIcon } from './open_timeline_button';
import { DeleteNoteButtonIcon } from './delete_note_button';
import { MarkdownRenderer } from '../../common/components/markdown_editor';
import { ADD_NOTE_LOADING_TEST_ID, NOTE_AVATAR_TEST_ID, NOTES_COMMENT_TEST_ID } from './test_ids';
import type { State } from '../../common/store';
import type { Note } from '../../../common/api/timeline';
import {
ReqStatus,
selectCreateNoteStatus,
selectNotesTablePendingDeleteIds,
} from '../store/notes.slice';
import { ReqStatus, selectCreateNoteStatus } from '../store/notes.slice';

export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.notes.addedANoteLabel', {
defaultMessage: 'added a note',
Expand Down Expand Up @@ -59,10 +54,6 @@ export interface NotesListProps {
*/
export const NotesList = memo(({ notes, options }: NotesListProps) => {
const createStatus = useSelector((state: State) => selectCreateNoteStatus(state));

const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds);
const isDeleteModalVisible = pendingDeleteIds.length > 0;

return (
<>
<EuiCommentList>
Expand Down Expand Up @@ -103,7 +94,6 @@ export const NotesList = memo(({ notes, options }: NotesListProps) => {
<EuiLoadingElastic size="xxl" data-test-subj={ADD_NOTE_LOADING_TEST_ID} />
)}
</EuiCommentList>
{isDeleteModalVisible && <DeleteConfirmModal refetch={false} />}
</>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { encode } from '@kbn/rison';
import type { State } from '../../../common/store';

import { PageScope } from '../../../data_view_manager/constants';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
Expand Down Expand Up @@ -136,6 +137,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
}) => {
const dispatch = useDispatch();
const { startTransaction } = useStartTransaction();
const noteIds = useSelector((state: State) => state.notes.ids);
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState<
Record<string, JSX.Element>
Expand Down Expand Up @@ -401,6 +403,21 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
fetchData();
}, [refetch, installPrepackagedTimelines, canWriteTimelines]);

useEffect(() => {
setItemIdToExpandedNotesRowMap((prev) => {
const newNotesMap: Record<string, JSX.Element> = { ...prev };
const noteIdsSet = new Set(noteIds);
for (const noteId of Object.keys(newNotesMap)) {
if (!noteIdsSet.has(noteId)) {
delete newNotesMap[noteId];
}
}

return newNotesMap;
});
refetch();
Comment thread
kelvtanv marked this conversation as resolved.
}, [noteIds, timelineSavedObjectId, refetch]);

return !isModal ? (
<OpenTimeline
data-test-subj={'open-timeline'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { cloneDeep } from 'lodash/fp';
import moment from 'moment';
import { mountWithI18nProvider } from '@kbn/test-jest-helpers';
import { fireEvent, screen, render, waitFor } from '@testing-library/react';
import { fireEvent, screen, render } from '@testing-library/react';
import React from 'react';
import '../../../../common/mock/formatted_relative';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
Expand All @@ -19,14 +19,16 @@ import { NotePreviews } from '.';
import { useDeleteNote } from './hooks/use_delete_note';
import { useUserPrivileges } from '../../../../common/components/user_privileges';

const mockDispatch = jest.fn();

jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/hooks/use_selector');

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

Expand Down Expand Up @@ -305,10 +307,9 @@ describe('NotePreviews', () => {
});

describe('Delete Notes', () => {
it('should delete note correctly', async () => {
it('should dispatch correct action on delete', async () => {
const timeline = {
...mockTimelineResults[0],
confirmingNoteId: 'noteId1',
};
(useDeepEqualSelector as jest.Mock).mockReturnValue(timeline);

Expand Down Expand Up @@ -342,11 +343,12 @@ describe('NotePreviews', () => {
);

fireEvent.click(screen.queryAllByTestId('delete-note')[0]);
await waitFor(() => {
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
});
expect(deleteMutateMock.mock.calls).toHaveLength(1);
expect(deleteMutateMock.mock.calls[0][0]).toBe('test-id-1');
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: 'notes/userSelectedNotesForDeletion',
payload: 'noteId1',
})
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@
*/

import { uniqBy } from 'lodash/fp';
import type { EuiConfirmModalProps } from '@elastic/eui';
import {
EuiAvatar,
EuiButtonIcon,
EuiCommentList,
EuiConfirmModal,
EuiScreenReaderOnly,
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n-react';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { userSelectedNotesForDeletion } from '../../../../notes';
import { PageScope } from '../../../../data_view_manager/constants';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useSelectedPatterns } from '../../../../data_view_manager/hooks/use_selected_patterns';
Expand All @@ -29,7 +27,7 @@ import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_detail
import type { TimelineResultNote } from '../types';
import { defaultToEmptyTag, getEmptyValue } from '../../../../common/components/empty_value';
import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
import { timelineActions, timelineSelectors } from '../../../store';
import { timelineSelectors } from '../../../store';
import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers';
import * as i18n from './translations';
import { TimelineId } from '../../../../../common/types/timeline';
Expand Down Expand Up @@ -98,90 +96,34 @@ const ToggleEventDetailsButtonComponent: React.FC<ToggleEventDetailsButtonProps>

const ToggleEventDetailsButton = React.memo(ToggleEventDetailsButtonComponent);

const DeleteNoteConfirm = React.memo<{
closeModal: EuiConfirmModalProps['onCancel'];
confirmModal: EuiConfirmModalProps['onConfirm'];
}>(({ closeModal, confirmModal }) => {
const modalTitleId = useGeneratedHtmlId();

return (
<EuiConfirmModal
aria-labelledby={modalTitleId}
title={i18n.DELETE_NOTE_CONFIRM}
titleProps={{ id: modalTitleId }}
onCancel={closeModal}
onConfirm={confirmModal}
cancelButtonText={i18n.CANCEL_DELETE_NOTE}
confirmButtonText={i18n.DELETE_NOTE}
buttonColor="danger"
defaultFocusedButton="confirm"
/>
);
});

DeleteNoteConfirm.displayName = 'DeleteNoteConfirm';

const DeleteNoteButton = React.memo<{
noteId?: string | null;
eventId?: string | null;
confirmingNoteId?: string | null;
savedObjectId?: string | null;
timelineId?: string;
eventIdToNoteIds?: Record<string, string[]>;
}>(({ noteId, eventId, confirmingNoteId, timelineId, eventIdToNoteIds, savedObjectId }) => {
}>(({ noteId, eventId, eventIdToNoteIds, savedObjectId }) => {
const dispatch = useDispatch();
const [showModal, setShowModal] = useState(false);
const { mutate, isLoading } = useDeleteNote(noteId, eventId, eventIdToNoteIds, savedObjectId);
const { isLoading } = useDeleteNote(noteId, eventId, eventIdToNoteIds, savedObjectId);

const handleOpenDeleteModal = useCallback(() => {
setShowModal(true);
dispatch(
timelineActions.setConfirmingNoteId({
confirmingNoteId: noteId,
id: timelineId ?? TimelineId.active,
})
);
}, [noteId, dispatch, timelineId]);

const handleCancelDelete = useCallback(() => {
setShowModal(false);
dispatch(
timelineActions.setConfirmingNoteId({
confirmingNoteId: null,
id: timelineId ?? TimelineId.active,
})
);
}, [dispatch, timelineId]);

const handleConfirmDelete = useCallback(() => {
mutate(savedObjectId);
setShowModal(false);
dispatch(
timelineActions.setConfirmingNoteId({
confirmingNoteId: null,
id: timelineId ?? TimelineId.active,
})
);
}, [mutate, savedObjectId, dispatch, timelineId]);
if (noteId && dispatch) {
dispatch(userSelectedNotesForDeletion(noteId));
}
}, [noteId, dispatch]);

const disableDelete = useMemo(() => {
return isLoading || savedObjectId == null;
}, [isLoading, savedObjectId]);
return (
<>
<EuiButtonIcon
title={i18n.DELETE_NOTE}
aria-label={i18n.DELETE_NOTE}
data-test-subj={'delete-note'}
color="text"
iconType="trash"
onClick={handleOpenDeleteModal}
disabled={disableDelete}
/>
{confirmingNoteId === noteId && showModal ? (
<DeleteNoteConfirm closeModal={handleCancelDelete} confirmModal={handleConfirmDelete} />
) : null}
</>
<EuiButtonIcon
title={i18n.DELETE_NOTE}
aria-label={i18n.DELETE_NOTE}
data-test-subj={'delete-note'}
color="text"
iconType="trash"
onClick={handleOpenDeleteModal}
disabled={disableDelete}
/>
);
});

Expand All @@ -192,15 +134,13 @@ const NoteActions = React.memo<{
timelineId?: string;
noteId?: string | null;
savedObjectId?: string | null;
confirmingNoteId?: string | null;
eventIdToNoteIds?: Record<string, string[]>;
showToggleEventDetailsAction?: boolean;
}>(
({
eventId,
timelineId,
noteId,
confirmingNoteId,
eventIdToNoteIds,
savedObjectId,
showToggleEventDetailsAction = true,
Expand All @@ -209,14 +149,14 @@ const NoteActions = React.memo<{
notesPrivileges: { crud: canCrudNotes },
} = useUserPrivileges();
const DeleteButton = canCrudNotes ? (
<DeleteNoteButton
noteId={noteId}
eventId={eventId}
confirmingNoteId={confirmingNoteId}
savedObjectId={savedObjectId}
timelineId={timelineId}
eventIdToNoteIds={eventIdToNoteIds}
/>
<>
<DeleteNoteButton
noteId={noteId}
eventId={eventId}
savedObjectId={savedObjectId}
eventIdToNoteIds={eventIdToNoteIds}
/>
</>
) : null;

return eventId && timelineId ? (
Expand Down Expand Up @@ -317,7 +257,6 @@ export const NotePreviews = React.memo<NotePreviewsProps>(
timelineId={timelineId}
noteId={note.noteId}
savedObjectId={note.savedObjectId}
confirmingNoteId={timeline?.confirmingNoteId}
eventIdToNoteIds={eventIdToNoteIds}
showToggleEventDetailsAction={showToggleEventDetailsAction}
/>
Expand All @@ -331,13 +270,7 @@ export const NotePreviews = React.memo<NotePreviewsProps>(
),
};
}),
[
eventIdToNoteIds,
notes,
timelineId,
timeline?.confirmingNoteId,
showToggleEventDetailsAction,
]
[eventIdToNoteIds, notes, timelineId, showToggleEventDetailsAction]
);

const commentList = useMemo(
Expand Down
Loading