From 5ee62333de49e546cfb1ee844c84433f0225e1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Sun, 22 Nov 2020 13:35:06 +0100 Subject: [PATCH] [Security Solution] Refactor Timeline flyout to take a full page (#82033) --- .../common/types/timeline/index.ts | 11 ++ .../cypress/tasks/security_main.ts | 3 +- .../__snapshots__/event_details.test.tsx.snap | 7 -- .../components/event_details/columns.tsx | 30 ++--- .../event_details/event_details.test.tsx | 9 -- .../event_details/event_details.tsx | 28 ++--- .../event_fields_browser.test.tsx | 13 ++- .../event_details/stateful_event_details.tsx | 23 +--- .../events_viewer/event_details_flyout.tsx | 82 ++++++++++++++ .../events_viewer/events_viewer.tsx | 53 ++++++--- .../common/components/events_viewer/index.tsx | 69 ++++++----- .../public/common/mock/global_state.ts | 3 +- .../public/common/mock/timeline_results.ts | 4 +- .../components/alerts_table/actions.test.tsx | 2 +- .../components/flyout/index.test.tsx | 30 +++-- .../timelines/components/flyout/index.tsx | 92 ++++++--------- .../pane/__snapshots__/index.test.tsx.snap | 8 +- .../components/flyout/pane/index.test.tsx | 43 +------ .../components/flyout/pane/index.tsx | 107 ++++-------------- .../flyout/pane/timeline_resize_handle.tsx | 22 ---- .../components/open_timeline/helpers.test.ts | 16 +-- .../timeline/body/actions/index.test.tsx | 48 +------- .../timeline/body/actions/index.tsx | 23 ++-- .../components/timeline/body/constants.ts | 2 - .../body/events/event_column_view.tsx | 3 - .../components/timeline/body/events/index.tsx | 21 +--- .../timeline/body/events/stateful_event.tsx | 93 ++++++--------- .../components/timeline/body/index.test.tsx | 3 +- .../components/timeline/body/index.tsx | 24 +--- .../timeline/body/stateful_body.tsx | 7 +- .../components/timeline/event_details.tsx | 64 +++++++++++ .../timeline/expandable_event/index.tsx | 94 +++++++++------ .../expandable_event/translations.tsx | 14 +++ .../timelines/components/timeline/index.tsx | 10 +- .../__snapshots__/index.test.tsx.snap | 18 --- .../timeline/skeleton_row/index.test.tsx | 45 -------- .../timeline/skeleton_row/index.tsx | 77 ------------- .../timelines/components/timeline/styles.tsx | 15 ++- .../components/timeline/timeline.test.tsx | 14 --- .../components/timeline/timeline.tsx | 76 ++++++++++--- .../containers/active_timeline_context.ts | 25 ++-- .../public/timelines/containers/index.tsx | 4 +- .../timelines/store/timeline/actions.ts | 21 ++-- .../timelines/store/timeline/defaults.ts | 3 +- .../timelines/store/timeline/epic.test.ts | 2 +- .../timelines/store/timeline/helpers.ts | 39 +------ .../public/timelines/store/timeline/model.ts | 6 +- .../timelines/store/timeline/reducer.test.ts | 7 +- .../timelines/store/timeline/reducer.ts | 37 ++---- 49 files changed, 618 insertions(+), 832 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 3888d37a547f7..967b3870cb9e0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -401,3 +401,14 @@ export const importTimelineResultSchema = runtimeTypes.exact( export type ImportTimelineResultSchema = runtimeTypes.TypeOf; export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom'; + +export interface TimelineExpandedEventType { + eventId: string; + indexName: string; + loading: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type EmptyObject = Record; + +export type TimelineExpandedEvent = TimelineExpandedEventType | EmptyObject; diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 6b1f3699d333a..dd01159e3029f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -10,10 +10,9 @@ export const openTimelineUsingToggle = () => { cy.get(TIMELINE_TOGGLE_BUTTON).click(); }; -export const openTimelineIfClosed = () => { +export const openTimelineIfClosed = () => cy.get(MAIN_PAGE).then(($page) => { if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) { openTimelineUsingToggle(); } }); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 2ae621e71a725..9ca9cd6cce389 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1544,12 +1544,5 @@ In other use cases the message field can be used to concatenate different values ] } /> - - Collapse event - `; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 7b6e9fb21a3e3..35cb8f7b1c91f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -12,8 +12,8 @@ import { EuiFlexItem, EuiIcon, EuiPanel, - EuiText, EuiToolTip, + EuiIconTip, } from '@elastic/eui'; import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; @@ -27,7 +27,6 @@ import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; import { DraggableFieldBadge } from '../draggables/field_badge'; import { FieldName } from '../../../timelines/components/fields_browser/field_name'; -import { SelectableText } from '../selectable_text'; import { OverflowField } from '../tables/helpers'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; @@ -90,6 +89,21 @@ export const getColumns = ({ ), }, + { + field: 'description', + name: '', + render: (description: string | null | undefined, data: EventFieldsData) => ( + + ), + sortable: true, + truncateText: true, + width: '30px', + }, { field: 'field', name: i18n.FIELD, @@ -187,18 +201,6 @@ export const getColumns = ({ ), }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: string | null | undefined, data: EventFieldsData) => ( - - {`${description || ''} ${getExampleText(data.example)}`} - - ), - sortable: true, - truncateText: true, - width: '50%', - }, { field: 'valuesConcatenated', name: i18n.BLANK, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index c3c7c864ac99b..bafe3df1a9cc7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -23,14 +23,12 @@ import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../link_to'); describe('EventDetails', () => { const mount = useMountAppended(); - const onEventToggled = jest.fn(); const defaultProps = { browserFields: mockBrowserFields, columnHeaders: defaultHeaders, data: mockDetailItemData, id: mockDetailItemDataId, view: 'table-view' as View, - onEventToggled, onUpdateColumns: jest.fn(), onViewSelected: jest.fn(), timelineId: 'test', @@ -66,12 +64,5 @@ describe('EventDetails', () => { wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text() ).toEqual('Table'); }); - - test('it invokes `onEventToggled` when the collapse button is clicked', () => { - wrapper.find('[data-test-subj="collapse"]').first().simulate('click'); - wrapper.update(); - - expect(onEventToggled).toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 074e6faf80c7d..a2a7182a768cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -5,7 +5,7 @@ */ import { EuiLink, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -15,9 +15,12 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; -import { COLLAPSE, COLLAPSE_EVENT } from '../../../timelines/components/timeline/body/translations'; -export type View = 'table-view' | 'json-view'; +export type View = EventsViewType.tableView | EventsViewType.jsonView; +export enum EventsViewType { + tableView = 'table-view', + jsonView = 'json-view', +} const CollapseLink = styled(EuiLink)` margin: 20px 0; @@ -30,10 +33,9 @@ interface Props { columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; id: string; - view: View; - onEventToggled: () => void; + view: EventsViewType; onUpdateColumns: OnUpdateColumns; - onViewSelected: (selected: View) => void; + onViewSelected: (selected: EventsViewType) => void; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } @@ -51,16 +53,19 @@ export const EventDetails = React.memo( data, id, view, - onEventToggled, onUpdateColumns, onViewSelected, timelineId, toggleColumn, }) => { + const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [ + onViewSelected, + ]); + const tabs: EuiTabbedContentTab[] = useMemo( () => [ { - id: 'table-view', + id: EventsViewType.tableView, name: i18n.TABLE, content: ( ( ), }, { - id: 'json-view', + id: EventsViewType.jsonView, name: i18n.JSON_VIEW, content: , }, @@ -88,11 +93,8 @@ export const EventDetails = React.memo( onViewSelected(e.id as View)} + onTabClick={handleTabClick} /> - - {COLLAPSE_EVENT} - ); } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 77d0ec330476c..0acf461828bc3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -21,7 +21,7 @@ describe('EventFieldsBrowser', () => { const mount = useMountAppended(); describe('column headers', () => { - ['Field', 'Value', 'Description'].forEach((header) => { + ['Field', 'Value'].forEach((header) => { test(`it renders the ${header} column header`, () => { const wrapper = mount( @@ -229,8 +229,15 @@ describe('EventFieldsBrowser', () => { ); - expect(wrapper.find('.euiTableRow').find('.euiTableRowCell').at(3).text()).toContain( - 'DescriptionDate/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' + expect( + wrapper + .find('.euiTableRow') + .find('.euiTableRowCell') + .at(1) + .find('EuiIconTip') + .prop('content') + ).toContain( + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx index bb74935d5703e..4730dc5c2264f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx @@ -4,49 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { BrowserFields } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; -import { EventDetails, View } from './event_details'; +import { EventDetails, EventsViewType, View } from './event_details'; interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; id: string; - onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } export const StatefulEventDetails = React.memo( - ({ - browserFields, - columnHeaders, - data, - id, - onEventToggled, - onUpdateColumns, - timelineId, - toggleColumn, - }) => { - const [view, setView] = useState('table-view'); + ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + // TODO: Move to the store + const [view, setView] = useState(EventsViewType.tableView); - const handleSetView = useCallback((newView) => setView(newView), []); return ( void; +} + +const EventDetailsFlyoutComponent: React.FC = ({ + browserFields, + docValueFields, + timelineId, + toggleColumn, +}) => { + const dispatch = useDispatch(); + const expandedEvent = useDeepEqualSelector( + (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {} + ); + + const handleClearSelection = useCallback(() => { + dispatch( + timelineActions.toggleExpandedEvent({ + timelineId, + event: {}, + }) + ); + }, [dispatch, timelineId]); + + if (!expandedEvent.eventId) { + return null; + } + + return ( + + + + + + + + + ); +}; + +export const EventDetailsFlyout = React.memo( + EventDetailsFlyoutComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.docValueFields, nextProps.docValueFields) && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn +); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 421b111d7941f..186083f1b05cd 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -36,7 +36,8 @@ import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useFullScreen } from '../../containers/use_full_screen'; -import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../common/types/timeline'; +import { GraphOverlay } from '../../../timelines/components/graph_overlay'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -76,6 +77,16 @@ const EventsContainerLoading = styled.div` flex-direction: column; `; +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + width: 100%; + overflow: hidden; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + /** * Hides stateful headerFilterGroup implementations, but prevents the component * from being unmounted, to preserve the state of the component @@ -280,21 +291,27 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - - - { - /** Hide the footer if Resolver is showing. */ - !graphEventId && ( + {graphEventId && ( + + )} + + +