mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Notes] - update notes management page columns (#194860)](https://github.com/elastic/kibana/pull/194860) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Philippe Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2024-10-09T21:41:02Z","message":"[Security Solution][Notes] - update notes management page columns (#194860)","sha":"3fa70e122c6a3c77edea3f2c47980403c1835256","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["backport","release_note:skip","v9.0.0","Team:Threat Hunting:Investigations","v8.16.0"],"title":"[Security Solution][Notes] - update notes management page columns","number":194860,"url":"https://github.com/elastic/kibana/pull/194860","mergeCommit":{"message":"[Security Solution][Notes] - update notes management page columns (#194860)","sha":"3fa70e122c6a3c77edea3f2c47980403c1835256"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194860","number":194860,"mergeCommit":{"message":"[Security Solution][Notes] - update notes management page columns (#194860)","sha":"3fa70e122c6a3c77edea3f2c47980403c1835256"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
parent
b44b90d3a8
commit
4944b9110f
17 changed files with 262 additions and 294 deletions
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ 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';
|
||||
|
@ -17,7 +18,11 @@ 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', {
|
||||
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { memo } from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { useInvestigateInTimeline } from '../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const OpenEventInTimeline: React.FC<{ eventId?: string | null }> = memo(({ eventId }) => {
|
||||
const ecsRowData = { event: { id: [eventId] }, _id: eventId } as Ecs;
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData });
|
||||
|
||||
return (
|
||||
<EuiLink onClick={investigateInTimelineAlertClick} data-test-subj="open-event-in-timeline">
|
||||
{i18n.VIEW_EVENT_IN_TIMELINE}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
|
||||
OpenEventInTimeline.displayName = 'OpenEventInTimeline';
|
|
@ -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');
|
||||
|
@ -27,7 +28,11 @@ describe('OpenFlyoutButtonIcon', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OpenFlyoutButtonIcon eventId={mockEventId} timelineId={mockTimelineId} />
|
||||
<OpenFlyoutButtonIcon
|
||||
eventId={mockEventId}
|
||||
timelineId={mockTimelineId}
|
||||
iconType="arrowRight"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -41,7 +46,11 @@ describe('OpenFlyoutButtonIcon', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<OpenFlyoutButtonIcon eventId={mockEventId} timelineId={mockTimelineId} />
|
||||
<OpenFlyoutButtonIcon
|
||||
eventId={mockEventId}
|
||||
timelineId={mockTimelineId}
|
||||
iconType="arrowRight"
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -54,7 +63,7 @@ describe('OpenFlyoutButtonIcon', () => {
|
|||
params: {
|
||||
id: mockEventId,
|
||||
indexName: 'test1,test2',
|
||||
scopeId: mockTimelineId,
|
||||
scopeId: TableId.alertsOnAlertsPage,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const BATCH_ACTIONS = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.batchActionsTitle',
|
||||
{
|
||||
defaultMessage: 'Bulk actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', {
|
||||
defaultMessage: 'Delete',
|
||||
});
|
||||
|
||||
export const DELETE_NOTES_MODAL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.deleteNotesModalTitle',
|
||||
{
|
||||
defaultMessage: 'Delete notes?',
|
||||
}
|
||||
);
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.deleteSelected',
|
||||
{
|
||||
defaultMessage: 'Delete selected notes',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
});
|
||||
|
||||
export const VIEW_EVENT_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.viewEventInTimeline',
|
||||
{
|
||||
defaultMessage: 'View event in timeline',
|
||||
}
|
||||
);
|
|
@ -4,9 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
UtilityBarGroup,
|
||||
UtilityBarText,
|
||||
|
@ -22,8 +24,28 @@ import {
|
|||
selectNotesTableSearch,
|
||||
userSelectedBulkDelete,
|
||||
} from '..';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const BATCH_ACTIONS = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.batchActionsTitle',
|
||||
{
|
||||
defaultMessage: 'Bulk actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_SELECTED = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.deleteSelected',
|
||||
{
|
||||
defaultMessage: 'Delete selected notes',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the utility bar for the notes management page
|
||||
*/
|
||||
export const NotesUtilityBar = React.memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const pagination = useSelector(selectNotesPagination);
|
||||
|
@ -49,7 +71,7 @@ export const NotesUtilityBar = React.memo(() => {
|
|||
icon="trash"
|
||||
key="DeleteItemKey"
|
||||
>
|
||||
{i18n.DELETE_SELECTED}
|
||||
{DELETE_SELECTED}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [deleteSelectedNotes, selectedItems.length]);
|
||||
|
@ -83,9 +105,7 @@ export const NotesUtilityBar = React.memo(() => {
|
|||
iconType="arrowDown"
|
||||
popoverContent={BulkActionPopoverContent}
|
||||
>
|
||||
<span data-test-subj="notes-management-utility-bar-action-button">
|
||||
{i18n.BATCH_ACTIONS}
|
||||
</span>
|
||||
<span data-test-subj="notes-management-utility-bar-action-button">{BATCH_ACTIONS}</span>
|
||||
</UtilityBarAction>
|
||||
<UtilityBarAction
|
||||
dataTestSubj="notes-management-utility-bar-refresh-button"
|
||||
|
@ -93,7 +113,7 @@ export const NotesUtilityBar = React.memo(() => {
|
|||
iconType="refresh"
|
||||
onClick={refresh}
|
||||
>
|
||||
{i18n.REFRESH}
|
||||
{REFRESH}
|
||||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
|
|
|
@ -4,12 +4,23 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { fetchNotesByDocumentIds } from '..';
|
||||
import { fetchNotesByDocumentIds } from '../store/notes.slice';
|
||||
|
||||
export const useFetchNotes = () => {
|
||||
export interface UseFetchNotesResult {
|
||||
/**
|
||||
* Function to fetch the notes for an array of documents
|
||||
*/
|
||||
onLoad: (events: Array<Partial<{ _id: string }>>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that returns a function to fetch the notes for an array of documents
|
||||
*/
|
||||
export const useFetchNotes = (): UseFetchNotesResult => {
|
||||
const dispatch = useDispatch();
|
||||
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
|
||||
'securitySolutionNotesEnabled'
|
||||
|
|
|
@ -6,11 +6,18 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import {
|
||||
EuiAvatar,
|
||||
EuiBasicTable,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers';
|
||||
import { css } from '@emotion/react';
|
||||
import { DeleteNoteButtonIcon } from '../components/delete_note_button';
|
||||
import { Title } from '../../common/components/header_page/title';
|
||||
// TODO unify this type from the api with the one in public/common/lib/note
|
||||
import type { Note } from '../../../common/api/timeline';
|
||||
|
@ -27,7 +34,6 @@ import {
|
|||
selectNotesTableSearch,
|
||||
selectFetchNotesStatus,
|
||||
selectNotesTablePendingDeleteIds,
|
||||
userSelectedRowForDeletion,
|
||||
selectFetchNotesError,
|
||||
ReqStatus,
|
||||
} from '..';
|
||||
|
@ -36,42 +42,67 @@ import { SearchRow } from '../components/search_row';
|
|||
import { NotesUtilityBar } from '../components/utility_bar';
|
||||
import { DeleteConfirmModal } from '../components/delete_confirm_modal';
|
||||
import * as i18n from './translations';
|
||||
import { OpenEventInTimeline } from '../components/open_event_in_timeline';
|
||||
import { OpenFlyoutButtonIcon } from '../components/open_flyout_button';
|
||||
import { OpenTimelineButtonIcon } from '../components/open_timeline_button';
|
||||
|
||||
const columns: (
|
||||
onOpenTimeline: (timelineId: string) => void
|
||||
) => Array<EuiBasicTableColumn<Note>> = (onOpenTimeline) => {
|
||||
return [
|
||||
{
|
||||
field: 'created',
|
||||
name: i18n.CREATED_COLUMN,
|
||||
sortable: true,
|
||||
render: (created: Note['created']) => <FormattedRelativePreferenceDate value={created} />,
|
||||
},
|
||||
{
|
||||
field: 'createdBy',
|
||||
name: i18n.CREATED_BY_COLUMN,
|
||||
},
|
||||
{
|
||||
field: 'eventId',
|
||||
name: i18n.EVENT_ID_COLUMN,
|
||||
sortable: true,
|
||||
render: (eventId: Note['eventId']) => <OpenEventInTimeline eventId={eventId} />,
|
||||
},
|
||||
{
|
||||
field: 'timelineId',
|
||||
name: i18n.TIMELINE_ID_COLUMN,
|
||||
render: (timelineId: Note['timelineId']) =>
|
||||
timelineId ? (
|
||||
<EuiLink onClick={() => onOpenTimeline(timelineId)}>{i18n.OPEN_TIMELINE}</EuiLink>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
name: i18n.NOTE_CONTENT_COLUMN,
|
||||
},
|
||||
];
|
||||
};
|
||||
const columns: Array<EuiBasicTableColumn<Note>> = [
|
||||
{
|
||||
name: i18n.ACTIONS_COLUMN,
|
||||
render: (note: Note) => (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
min-width: 24px;
|
||||
`}
|
||||
>
|
||||
{note.eventId ? (
|
||||
<OpenFlyoutButtonIcon
|
||||
eventId={note.eventId}
|
||||
timelineId={note.timelineId}
|
||||
iconType="expand"
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
min-width: 24px;
|
||||
`}
|
||||
>
|
||||
<>{note.timelineId ? <OpenTimelineButtonIcon note={note} /> : null}</>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
min-width: 24px;
|
||||
`}
|
||||
>
|
||||
<DeleteNoteButtonIcon note={note} index={0} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '72px',
|
||||
},
|
||||
{
|
||||
field: 'createdBy',
|
||||
name: i18n.CREATED_BY_COLUMN,
|
||||
render: (createdBy: Note['createdBy']) => <EuiAvatar name={createdBy || ''} size="s" />,
|
||||
width: '100px',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
name: i18n.NOTE_CONTENT_COLUMN,
|
||||
},
|
||||
{
|
||||
field: 'created',
|
||||
name: i18n.CREATED_COLUMN,
|
||||
sortable: true,
|
||||
render: (created: Note['created']) => <FormattedRelativePreferenceDate value={created} />,
|
||||
width: '225px',
|
||||
},
|
||||
];
|
||||
|
||||
const pageSizeOptions = [10, 25, 50, 100];
|
||||
|
||||
|
@ -129,13 +160,6 @@ export const NoteManagementPage = () => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const selectRowForDeletion = useCallback(
|
||||
(id: string) => {
|
||||
dispatch(userSelectedRowForDeletion(id));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectionChange = useCallback(
|
||||
(selection: Note[]) => {
|
||||
const rowIds = selection.map((item) => item.noteId);
|
||||
|
@ -148,39 +172,6 @@ export const NoteManagementPage = () => {
|
|||
return item.noteId;
|
||||
}, []);
|
||||
|
||||
const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineDisabled'
|
||||
);
|
||||
const queryTimelineById = useQueryTimelineById();
|
||||
const openTimeline = useCallback(
|
||||
(timelineId: string) =>
|
||||
queryTimelineById({
|
||||
timelineId,
|
||||
unifiedComponentsInTimelineDisabled,
|
||||
}),
|
||||
[queryTimelineById, unifiedComponentsInTimelineDisabled]
|
||||
);
|
||||
|
||||
const columnWithActions = useMemo(() => {
|
||||
const actions: Array<DefaultItemAction<Note>> = [
|
||||
{
|
||||
name: i18n.DELETE,
|
||||
description: i18n.DELETE_SINGLE_NOTE_DESCRIPTION,
|
||||
color: 'primary',
|
||||
icon: 'trash',
|
||||
type: 'icon',
|
||||
onClick: (note: Note) => selectRowForDeletion(note.noteId),
|
||||
},
|
||||
];
|
||||
return [
|
||||
...columns(openTimeline),
|
||||
{
|
||||
name: 'actions',
|
||||
actions,
|
||||
},
|
||||
];
|
||||
}, [selectRowForDeletion, openTimeline]);
|
||||
|
||||
const currentPagination = useMemo(() => {
|
||||
return {
|
||||
pageIndex: pagination.page - 1,
|
||||
|
@ -223,7 +214,7 @@ export const NoteManagementPage = () => {
|
|||
<EuiBasicTable
|
||||
items={notes}
|
||||
pagination={currentPagination}
|
||||
columns={columnWithActions}
|
||||
columns={columns}
|
||||
onChange={onTableChange}
|
||||
selection={selection}
|
||||
sorting={sorting}
|
||||
|
|
|
@ -11,6 +11,12 @@ export const NOTES = i18n.translate('xpack.securitySolution.notes.management.tit
|
|||
defaultMessage: 'Notes',
|
||||
});
|
||||
|
||||
export const ACTIONS_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.actionsColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Actions',
|
||||
}
|
||||
);
|
||||
export const CREATED_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.createdColumnTitle',
|
||||
{
|
||||
|
@ -25,20 +31,6 @@ export const CREATED_BY_COLUMN = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EVENT_ID_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.eventIdColumnTitle',
|
||||
{
|
||||
defaultMessage: 'View Document',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_ID_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.timelineColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const NOTE_CONTENT_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.noteContentColumnTitle',
|
||||
{
|
||||
|
@ -50,13 +42,6 @@ export const DELETE = i18n.translate('xpack.securitySolution.notes.management.de
|
|||
defaultMessage: 'Delete',
|
||||
});
|
||||
|
||||
export const DELETE_SINGLE_NOTE_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.notes.management.deleteDescription',
|
||||
{
|
||||
defaultMessage: 'Delete this note',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE_ERROR = i18n.translate('xpack.securitySolution.notes.management.tableError', {
|
||||
defaultMessage: 'Unable to load notes',
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ import {
|
|||
userSelectedPage,
|
||||
userSelectedPerPage,
|
||||
userSelectedRow,
|
||||
userSelectedRowForDeletion,
|
||||
userSelectedNotesForDeletion,
|
||||
userSortedNotes,
|
||||
selectSortedNotesByDocumentId,
|
||||
fetchNotesBySavedObjectIds,
|
||||
|
@ -533,9 +533,9 @@ describe('notesSlice', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('userSelectedRowForDeletion', () => {
|
||||
it('should set correct id when user selects a row', () => {
|
||||
const action = { type: userSelectedRowForDeletion.type, payload: '1' };
|
||||
describe('userSelectedNotesForDeletion', () => {
|
||||
it('should set correct id when user selects a note to delete', () => {
|
||||
const action = { type: userSelectedNotesForDeletion.type, payload: '1' };
|
||||
|
||||
expect(notesReducer(initalEmptyState, action)).toEqual({
|
||||
...initalEmptyState,
|
||||
|
|
|
@ -193,7 +193,7 @@ const notesSlice = createSlice({
|
|||
userClosedDeleteModal: (state) => {
|
||||
state.pendingDeleteIds = [];
|
||||
},
|
||||
userSelectedRowForDeletion: (state, action: { payload: string }) => {
|
||||
userSelectedNotesForDeletion: (state, action: { payload: string }) => {
|
||||
state.pendingDeleteIds = [action.payload];
|
||||
},
|
||||
userSelectedBulkDelete: (state) => {
|
||||
|
@ -391,6 +391,6 @@ export const {
|
|||
userSearchedNotes,
|
||||
userSelectedRow,
|
||||
userClosedDeleteModal,
|
||||
userSelectedRowForDeletion,
|
||||
userSelectedNotesForDeletion,
|
||||
userSelectedBulkDelete,
|
||||
} = notesSlice.actions;
|
||||
|
|
|
@ -39634,18 +39634,13 @@
|
|||
"xpack.securitySolution.notes.management.createdByColumnTitle": "Créé par",
|
||||
"xpack.securitySolution.notes.management.createdColumnTitle": "Créé",
|
||||
"xpack.securitySolution.notes.management.deleteAction": "Supprimer",
|
||||
"xpack.securitySolution.notes.management.deleteDescription": "Supprimer cette note",
|
||||
"xpack.securitySolution.notes.management.deleteNotesCancel": "Annuler",
|
||||
"xpack.securitySolution.notes.management.deleteNotesConfirm": "Voulez-vous vraiment supprimer {selectedNotes} {selectedNotes, plural, one {note} other {notes}} ?",
|
||||
"xpack.securitySolution.notes.management.deleteNotesModalTitle": "Supprimer les notes ?",
|
||||
"xpack.securitySolution.notes.management.deleteSelected": "Supprimer les notes sélectionnées",
|
||||
"xpack.securitySolution.notes.management.eventIdColumnTitle": "Afficher le document",
|
||||
"xpack.securitySolution.notes.management.noteContentColumnTitle": "Contenu de la note",
|
||||
"xpack.securitySolution.notes.management.openTimeline": "Ouvrir la chronologie",
|
||||
"xpack.securitySolution.notes.management.refresh": "Actualiser",
|
||||
"xpack.securitySolution.notes.management.tableError": "Impossible de charger les notes",
|
||||
"xpack.securitySolution.notes.management.timelineColumnTitle": "Chronologie",
|
||||
"xpack.securitySolution.notes.management.viewEventInTimeline": "Afficher l'événement dans la chronologie",
|
||||
"xpack.securitySolution.notes.noteLabel": "Note",
|
||||
"xpack.securitySolution.notes.notesTitle": "Notes",
|
||||
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "Filtre par utilisateur ou note",
|
||||
|
|
|
@ -39378,18 +39378,13 @@
|
|||
"xpack.securitySolution.notes.management.createdByColumnTitle": "作成者",
|
||||
"xpack.securitySolution.notes.management.createdColumnTitle": "作成済み",
|
||||
"xpack.securitySolution.notes.management.deleteAction": "削除",
|
||||
"xpack.securitySolution.notes.management.deleteDescription": "このメモを削除",
|
||||
"xpack.securitySolution.notes.management.deleteNotesCancel": "キャンセル",
|
||||
"xpack.securitySolution.notes.management.deleteNotesConfirm": "{selectedNotes} {selectedNotes, plural, other {件のメモ}}を削除しますか?",
|
||||
"xpack.securitySolution.notes.management.deleteNotesModalTitle": "メモを削除しますか?",
|
||||
"xpack.securitySolution.notes.management.deleteSelected": "選択したメモを削除",
|
||||
"xpack.securitySolution.notes.management.eventIdColumnTitle": "ドキュメンテーションを表示",
|
||||
"xpack.securitySolution.notes.management.noteContentColumnTitle": "メモコンテンツ",
|
||||
"xpack.securitySolution.notes.management.openTimeline": "タイムラインを開く",
|
||||
"xpack.securitySolution.notes.management.refresh": "更新",
|
||||
"xpack.securitySolution.notes.management.tableError": "メモを読み込めません",
|
||||
"xpack.securitySolution.notes.management.timelineColumnTitle": "Timeline",
|
||||
"xpack.securitySolution.notes.management.viewEventInTimeline": "タイムラインでイベントを表示",
|
||||
"xpack.securitySolution.notes.noteLabel": "注",
|
||||
"xpack.securitySolution.notes.notesTitle": "メモ",
|
||||
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター",
|
||||
|
|
|
@ -39423,18 +39423,13 @@
|
|||
"xpack.securitySolution.notes.management.createdByColumnTitle": "创建者",
|
||||
"xpack.securitySolution.notes.management.createdColumnTitle": "创建时间",
|
||||
"xpack.securitySolution.notes.management.deleteAction": "删除",
|
||||
"xpack.securitySolution.notes.management.deleteDescription": "删除此备注",
|
||||
"xpack.securitySolution.notes.management.deleteNotesCancel": "取消",
|
||||
"xpack.securitySolution.notes.management.deleteNotesConfirm": "是否确定要删除 {selectedNotes} 个{selectedNotes, plural, other {备注}}?",
|
||||
"xpack.securitySolution.notes.management.deleteNotesModalTitle": "删除备注?",
|
||||
"xpack.securitySolution.notes.management.deleteSelected": "删除所选备注",
|
||||
"xpack.securitySolution.notes.management.eventIdColumnTitle": "查看文档",
|
||||
"xpack.securitySolution.notes.management.noteContentColumnTitle": "备注内容",
|
||||
"xpack.securitySolution.notes.management.openTimeline": "打开时间线",
|
||||
"xpack.securitySolution.notes.management.refresh": "刷新",
|
||||
"xpack.securitySolution.notes.management.tableError": "无法加载备注",
|
||||
"xpack.securitySolution.notes.management.timelineColumnTitle": "时间线",
|
||||
"xpack.securitySolution.notes.management.viewEventInTimeline": "在时间线中查看事件",
|
||||
"xpack.securitySolution.notes.noteLabel": "备注",
|
||||
"xpack.securitySolution.notes.notesTitle": "备注",
|
||||
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue