mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Timelines] Notes table links (#187868)
## 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.

### 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))
This commit is contained in:
parent
32f6a78933
commit
209b0c52cb
6 changed files with 90 additions and 65 deletions
|
@ -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';
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
@ -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';
|
||||
import type { OpenTimelineProps } from '../../timelines/components/open_timeline/types';
|
||||
import { OpenEventInTimeline } from '../components/open_event_in_timeline';
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
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];
|
||||
|
||||
|
@ -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);
|
||||
|
@ -147,13 +164,13 @@ export const NoteManagementPage = () => {
|
|||
},
|
||||
];
|
||||
return [
|
||||
...columns,
|
||||
...columns(onOpenTimeline),
|
||||
{
|
||||
name: 'actions',
|
||||
actions,
|
||||
},
|
||||
];
|
||||
}, [selectRowForDeletion]);
|
||||
}, [selectRowForDeletion, onOpenTimeline]);
|
||||
|
||||
const currentPagination = useMemo(() => {
|
||||
return {
|
||||
|
|
|
@ -312,7 +312,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<NoteManagementPage />
|
||||
<NoteManagementPage onOpenTimeline={onOpenTimeline} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -37,8 +37,6 @@ export const links: LinkItem = {
|
|||
defaultMessage: 'Visualize and delete notes.',
|
||||
}),
|
||||
path: `${TIMELINES_PATH}/notes`,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
experimentalKey: 'securitySolutionNotesEnabled',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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}>
|
||||
|
@ -51,8 +27,4 @@ export const routes: SecuritySubPluginRoutes = [
|
|||
path: TIMELINES_PATH,
|
||||
component: TimelinesRoutes,
|
||||
},
|
||||
{
|
||||
path: NOTES_MANAGEMENT_PATH,
|
||||
component: NoteManagementContainer,
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue