[8.x] [Security Solution][Notes] - update notes management page columns (#194860) (#195683)

# 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:
Kibana Machine 2024-10-10 10:28:42 +11:00 committed by GitHub
parent b44b90d3a8
commit 4944b9110f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 262 additions and 294 deletions

View file

@ -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>
);
});

View file

@ -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]
);

View file

@ -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 />}
</>
);
});

View file

@ -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';

View file

@ -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,
},
},
});

View file

@ -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';

View file

@ -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)}
/>
);

View file

@ -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',
}
);

View file

@ -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>

View file

@ -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'

View file

@ -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}

View file

@ -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',
});

View file

@ -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,

View file

@ -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;

View file

@ -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",

View file

@ -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": "ユーザーまたはメモでフィルター",

View file

@ -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": "按用户或备注筛选",