Skip to content

Commit

Permalink
[Security Solution][Notes] - update notes management page columns
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeOberti committed Oct 9, 2024
1 parent a31b16e commit 8a21394
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 295 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiConfirmModal } from '@elastic/eui';
import * as i18n from './translations';
import { i18n } from '@kbn/i18n';
import {
deleteNotes,
userClosedDeleteModal,
Expand All @@ -16,6 +16,25 @@ import {
ReqStatus,
} from '..';

export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', {
defaultMessage: 'Delete',
});
export const DELETE_NOTES_CONFIRM = (selectedNotes: number) =>
i18n.translate('xpack.securitySolution.notes.management.deleteNotesConfirm', {
values: { selectedNotes },
defaultMessage:
'Are you sure you want to delete {selectedNotes} {selectedNotes, plural, one {note} other {notes}}?',
});
export const DELETE_NOTES_CANCEL = i18n.translate(
'xpack.securitySolution.notes.management.deleteNotesCancel',
{
defaultMessage: 'Cancel',
}
);

/**
* Renders a confirmation modal to delete notes in the notes management page
*/
export const DeleteConfirmModal = React.memo(() => {
const dispatch = useDispatch();
const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds);
Expand All @@ -33,16 +52,16 @@ export const DeleteConfirmModal = React.memo(() => {
return (
<EuiConfirmModal
aria-labelledby={'delete-notes-modal'}
title={i18n.DELETE_NOTES_MODAL_TITLE}
title={DELETE}
onCancel={onCancel}
onConfirm={onConfirm}
isLoading={deleteLoading}
cancelButtonText={i18n.DELETE_NOTES_CANCEL}
confirmButtonText={i18n.DELETE}
cancelButtonText={DELETE_NOTES_CANCEL}
confirmButtonText={DELETE}
buttonColor="danger"
defaultFocusedButton="confirm"
>
{i18n.DELETE_NOTES_CONFIRM(pendingDeleteIds.length)}
{DELETE_NOTES_CONFIRM(pendingDeleteIds.length)}
</EuiConfirmModal>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { DELETE_NOTE_BUTTON_TEST_ID } from './test_ids';
import type { State } from '../../common/store';
import type { Note } from '../../../common/api/timeline';
import {
deleteNotes,
ReqStatus,
selectDeleteNotesError,
selectDeleteNotesStatus,
userSelectedNotesForDeletion,
} from '../store/notes.slice';
import { useAppToasts } from '../../common/hooks/use_app_toasts';

Expand All @@ -42,7 +42,8 @@ export interface DeleteNoteButtonIconProps {
}

/**
* Renders a button to delete a note
* Renders a button to delete a note.
* This button works in combination with the DeleteConfirmModal.
*/
export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconProps) => {
const dispatch = useDispatch();
Expand All @@ -54,8 +55,8 @@ export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconP

const deleteNoteFc = useCallback(
(noteId: string) => {
dispatch(userSelectedNotesForDeletion(noteId));
setDeletingNoteId(noteId);
dispatch(deleteNotes({ ids: [noteId] }));
},
[dispatch]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ 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 } from '../store/notes.slice';
import {
ReqStatus,
selectCreateNoteStatus,
selectNotesTablePendingDeleteIds,
} from '../store/notes.slice';
import { useUserPrivileges } from '../../common/components/user_privileges';

export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.notes.addedANoteLabel', {
Expand Down Expand Up @@ -59,41 +64,51 @@ 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>
{notes.map((note, index) => (
<EuiComment
data-test-subj={`${NOTES_COMMENT_TEST_ID}-${index}`}
key={note.noteId}
username={note.createdBy}
timestamp={<>{note.created && <FormattedRelative value={new Date(note.created)} />}</>}
event={ADDED_A_NOTE}
actions={
<>
{note.eventId && !options?.hideFlyoutIcon && (
<OpenFlyoutButtonIcon eventId={note.eventId} timelineId={note.timelineId} />
)}
{note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && (
<OpenTimelineButtonIcon note={note} index={index} />
)}
{canDeleteNotes && <DeleteNoteButtonIcon note={note} index={index} />}
</>
}
timelineAvatar={
<EuiAvatar
data-test-subj={`${NOTE_AVATAR_TEST_ID}-${index}`}
size="l"
name={note.updatedBy || '?'}
/>
}
>
<MarkdownRenderer>{note.note || ''}</MarkdownRenderer>
</EuiComment>
))}
{createStatus === ReqStatus.Loading && (
<EuiLoadingElastic size="xxl" data-test-subj={ADD_NOTE_LOADING_TEST_ID} />
)}
</EuiCommentList>
<>
<EuiCommentList>
{notes.map((note, index) => (
<EuiComment
data-test-subj={`${NOTES_COMMENT_TEST_ID}-${index}`}
key={note.noteId}
username={note.createdBy}
timestamp={<>{note.created && <FormattedRelative value={new Date(note.created)} />}</>}
event={ADDED_A_NOTE}
actions={
<>
{note.eventId && !options?.hideFlyoutIcon && (
<OpenFlyoutButtonIcon
eventId={note.eventId}
timelineId={note.timelineId}
iconType="arrowRight"
/>
)}
{note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && (
<OpenTimelineButtonIcon note={note} index={index} />
)}
{canDeleteNotes && <DeleteNoteButtonIcon note={note} index={index} />}
</>
}
timelineAvatar={
<EuiAvatar
data-test-subj={`${NOTE_AVATAR_TEST_ID}-${index}`}
size="l"
name={note.updatedBy || '?'}
/>
}
>
<MarkdownRenderer>{note.note || ''}</MarkdownRenderer>
</EuiComment>
))}
{createStatus === ReqStatus.Loading && (
<EuiLoadingElastic size="xxl" data-test-subj={ADD_NOTE_LOADING_TEST_ID} />
)}
</EuiCommentList>
{isDeleteModalVisible && <DeleteConfirmModal />}
</>
);
});

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { OpenFlyoutButtonIcon } from './open_flyout_button';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys';
import { useSourcererDataView } from '../../sourcerer/containers';
import { TableId } from '@kbn/securitysolution-data-table';

jest.mock('@kbn/expandable-flyout');
jest.mock('../../sourcerer/containers');
Expand All @@ -27,7 +28,11 @@ describe('OpenFlyoutButtonIcon', () => {

const { getByTestId } = render(
<TestProviders>
<OpenFlyoutButtonIcon eventId={mockEventId} timelineId={mockTimelineId} />
<OpenFlyoutButtonIcon
eventId={mockEventId}
timelineId={mockTimelineId}
iconType="arrowRight"
/>
</TestProviders>
);

Expand All @@ -41,7 +46,11 @@ describe('OpenFlyoutButtonIcon', () => {

const { getByTestId } = render(
<TestProviders>
<OpenFlyoutButtonIcon eventId={mockEventId} timelineId={mockTimelineId} />
<OpenFlyoutButtonIcon
eventId={mockEventId}
timelineId={mockTimelineId}
iconType="arrowRight"
/>
</TestProviders>
);

Expand All @@ -54,7 +63,7 @@ describe('OpenFlyoutButtonIcon', () => {
params: {
id: mockEventId,
indexName: 'test1,test2',
scopeId: mockTimelineId,
scopeId: TableId.alertsOnAlertsPage,
},
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
*/

import React, { memo, useCallback } from 'react';
import type { IconType } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TableId } from '@kbn/securitysolution-data-table';
import { OPEN_FLYOUT_BUTTON_TEST_ID } from './test_ids';
import { useSourcererDataView } from '../../sourcerer/containers';
import { SourcererScopeName } from '../../sourcerer/store/model';
Expand All @@ -31,44 +33,51 @@ export interface OpenFlyoutButtonIconProps {
* Id of the timeline to pass to the flyout for scope
*/
timelineId: string;
/**
* Icon type to render in the button
*/
iconType: IconType;
}

/**
* Renders a button to open the alert and event details flyout
* Renders a button to open the alert and event details flyout.
* This component is meant to be used in timeline and the notes management page, where the cell actions are more basic (no filter in/out).
*/
export const OpenFlyoutButtonIcon = memo(({ eventId, timelineId }: OpenFlyoutButtonIconProps) => {
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
export const OpenFlyoutButtonIcon = memo(
({ eventId, timelineId, iconType }: OpenFlyoutButtonIconProps) => {
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);

const { telemetry } = useKibana().services;
const { openFlyout } = useExpandableFlyoutApi();
const { telemetry } = useKibana().services;
const { openFlyout } = useExpandableFlyoutApi();

const handleClick = useCallback(() => {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName: selectedPatterns.join(','),
scopeId: timelineId,
const handleClick = useCallback(() => {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName: selectedPatterns.join(','),
scopeId: TableId.alertsOnAlertsPage, // TODO we should update the flyout's code to separate scopeId and preview
},
},
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
}, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]);
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
}, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]);

return (
<EuiButtonIcon
data-test-subj={OPEN_FLYOUT_BUTTON_TEST_ID}
title={OPEN_FLYOUT_BUTTON}
aria-label={OPEN_FLYOUT_BUTTON}
color="text"
iconType="arrowRight"
onClick={handleClick}
/>
);
});
return (
<EuiButtonIcon
data-test-subj={OPEN_FLYOUT_BUTTON_TEST_ID}
title={OPEN_FLYOUT_BUTTON}
aria-label={OPEN_FLYOUT_BUTTON}
color="text"
iconType={iconType}
onClick={handleClick}
/>
);
}
);

OpenFlyoutButtonIcon.displayName = 'OpenFlyoutButtonIcon';
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@

import React, { memo, useCallback } from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers';
import { OPEN_TIMELINE_BUTTON_TEST_ID } from './test_ids';
import type { Note } from '../../../common/api/timeline';

const OPEN_TIMELINE = i18n.translate('xpack.securitySolution.notes.management.openTimelineButton', {
defaultMessage: 'Open saved timeline',
});

export interface OpenTimelineButtonIconProps {
/**
* The note that contains the id of the timeline to open
Expand All @@ -20,7 +25,7 @@ export interface OpenTimelineButtonIconProps {
/**
* The index of the note in the list of notes (used to have unique data-test-subj)
*/
index: number;
index?: number;
}

/**
Expand All @@ -47,10 +52,10 @@ export const OpenTimelineButtonIcon = memo(({ note, index }: OpenTimelineButtonI
return (
<EuiButtonIcon
data-test-subj={`${OPEN_TIMELINE_BUTTON_TEST_ID}-${index}`}
title="Open timeline"
aria-label="Open timeline"
title={OPEN_TIMELINE}
aria-label={OPEN_TIMELINE}
color="text"
iconType="timeline"
iconType="timelineWithArrow"
onClick={() => openTimeline(note)}
/>
);
Expand Down
Loading

0 comments on commit 8a21394

Please sign in to comment.