Skip to content

Commit

Permalink
[Security Solution] [Timelines] Notes table links (#187868)
Browse files Browse the repository at this point in the history
## Summary

This pr changes the timeline id cell to be a link to open the saved
timeline a note is a part of if timelineId exists, instead of just
showing the id as a plain string. Also updates the event column to a
link that opens a new timeline containing just the event a note is
associated with.

![image](https://github.com/elastic/kibana/assets/56408403/e1c577e6-deb6-4daf-8d94-78fcc400c041)


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
  • Loading branch information
kqualters-elastic authored Jul 10, 2024
1 parent 32f6a78 commit 209b0c5
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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';
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ export const CREATED_BY_COLUMN = i18n.translate(
export const EVENT_ID_COLUMN = i18n.translate(
'xpack.securitySolution.notes.management.eventIdColumnTitle',
{
defaultMessage: 'Document ID',
defaultMessage: 'View Document',
}
);

export const TIMELINE_ID_COLUMN = i18n.translate(
'xpack.securitySolution.notes.management.timelineIdColumnTitle',
'xpack.securitySolution.notes.management.timelineColumnTitle',
{
defaultMessage: 'Timeline ID',
defaultMessage: 'Timeline',
}
);

Expand Down Expand Up @@ -102,3 +102,17 @@ export const DELETE_SELECTED = i18n.translate(
export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', {
defaultMessage: 'Refresh',
});

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

export const VIEW_EVENT_IN_TIMELINE = i18n.translate(
'xpack.securitySolution.notes.management.viewEventInTimeline',
{
defaultMessage: 'View event in timeline',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { useCallback, useMemo, useEffect } from 'react';
import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui';
import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui';
import { EuiBasicTable, EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
// TODO unify this type from the api with the one in public/common/lib/note
import type { Note } from '../../../common/api/timeline';
Expand All @@ -33,32 +33,45 @@ import { SearchRow } from '../components/search_row';
import { NotesUtilityBar } from '../components/utility_bar';
import { DeleteConfirmModal } from '../components/delete_confirm_modal';
import * as i18n from '../components/translations';

const columns: Array<EuiBasicTableColumn<Note>> = [
{
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,
},
{
field: 'timelineId',
name: i18n.TIMELINE_ID_COLUMN,
},
{
field: 'note',
name: i18n.NOTE_CONTENT_COLUMN,
},
];
import type { OpenTimelineProps } from '../../timelines/components/open_timeline/types';
import { OpenEventInTimeline } from '../components/open_event_in_timeline';

const columns: (
onOpenTimeline: OpenTimelineProps['onOpenTimeline']
) => 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, duplicate: false })}>
{i18n.OPEN_TIMELINE}
</EuiLink>
) : null,
},
{
field: 'note',
name: i18n.NOTE_CONTENT_COLUMN,
},
];
};

const pageSizeOptions = [50, 25, 10, 0];

Expand All @@ -67,7 +80,11 @@ const pageSizeOptions = [50, 25, 10, 0];
* This component uses the same slices of state as the notes functionality of the rest of the Security Solution applicaiton.
* Therefore, changes made in this page (like fetching or deleting notes) will have an impact everywhere.
*/
export const NoteManagementPage = () => {
export const NoteManagementPage = ({
onOpenTimeline,
}: {
onOpenTimeline: OpenTimelineProps['onOpenTimeline'];
}) => {
const dispatch = useDispatch();
const notes = useSelector(selectAllNotes);
const pagination = useSelector(selectNotesPagination);
Expand Down Expand Up @@ -147,13 +164,13 @@ export const NoteManagementPage = () => {
},
];
return [
...columns,
...columns(onOpenTimeline),
{
name: 'actions',
actions,
},
];
}, [selectRowForDeletion]);
}, [selectRowForDeletion, onOpenTimeline]);

const currentPagination = useMemo(() => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
/>
</>
) : (
<NoteManagementPage />
<NoteManagementPage onOpenTimeline={onOpenTimeline} />
)}
</div>
</>
Expand Down
2 changes: 0 additions & 2 deletions x-pack/plugins/security_solution/public/timelines/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export const links: LinkItem = {
defaultMessage: 'Visualize and delete notes.',
}),
path: `${TIMELINES_PATH}/notes`,
skipUrlState: true,
hideTimeline: true,
experimentalKey: 'securitySolutionNotesEnabled',
},
],
Expand Down
32 changes: 2 additions & 30 deletions x-pack/plugins/security_solution/public/timelines/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,15 @@
* 2.0.
*/

import React, { memo } from 'react';
import React from 'react';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';

import { Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import { SpyRoute } from '../common/utils/route/spy_routes';
import { NotFoundPage } from '../app/404';
import { NoteManagementPage } from '../notes/pages/note_management_page';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityPageName } from '../app/types';
import type { SecuritySubPluginRoutes } from '../app/types';
import { NOTES_MANAGEMENT_PATH, TIMELINES_PATH } from '../../common/constants';
import { TIMELINES_PATH } from '../../common/constants';
import { Timelines } from './pages';

const NoteManagementTelemetry = () => (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.notesManagement}>
<NoteManagementPage />
<SpyRoute pageName={SecurityPageName.notesManagement} />
</TrackApplicationView>
</PluginTemplateWrapper>
);

const NoteManagementContainer = memo(() => {
return (
<Switch>
<Route path={NOTES_MANAGEMENT_PATH} exact component={NoteManagementTelemetry} />
<Route component={NotFoundPage} />
</Switch>
);
});
NoteManagementContainer.displayName = 'NoteManagementContainer';

const TimelinesRoutes = () => (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.timelines}>
Expand All @@ -51,8 +27,4 @@ export const routes: SecuritySubPluginRoutes = [
path: TIMELINES_PATH,
component: TimelinesRoutes,
},
{
path: NOTES_MANAGEMENT_PATH,
component: NoteManagementContainer,
},
];

0 comments on commit 209b0c5

Please sign in to comment.