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][Notes] - update notes management page columns #194860

Merged
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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