From 4cd38e5c123d78a37af7ede2236a387676631af4 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 27 Oct 2020 17:30:16 +0100 Subject: [PATCH 01/58] [WIP] --- .../public/common/mock/global_state.ts | 2 - .../timelines/components/flyout/index.tsx | 92 ++++++--------- .../components/flyout/pane/index.tsx | 107 ++++-------------- .../components/timeline/body/constants.ts | 2 - .../timelines/store/timeline/defaults.ts | 2 - .../public/timelines/store/timeline/model.ts | 3 - .../timelines/store/timeline/reducer.test.ts | 6 +- 7 files changed, 54 insertions(+), 160 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 0944b6aa27f67..8b539cb8c8f84 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMELINE_WIDTH } from '../../timelines/components/timeline/body/constants'; import { Direction, FlowTarget, @@ -238,7 +237,6 @@ export const mockGlobalState: State = { pinnedEventsSaveObject: {}, itemsPerPageOptions: [5, 10, 20], sort: { columnId: '@timestamp', sortDirection: Direction.desc }, - width: DEFAULT_TIMELINE_WIDTH, isSaving: false, version: null, status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 7d0f5995afc3b..f5ad6264f95e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -6,17 +6,14 @@ import { EuiBadge } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { State } from '../../../common/store'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions, timelineSelectors } from '../../store/timeline'; -import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; -import { StatefulTimeline } from '../timeline'; -import { TimelineById } from '../../store/timeline/types'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; export const Badge = (styled(EuiBadge)` position: absolute; @@ -40,66 +37,41 @@ interface OwnProps { usersViewing: string[]; } -type Props = OwnProps & ProsFromRedux; - -export const FlyoutComponent = React.memo( - ({ dataProviders, show = true, showTimeline, timelineId, usersViewing, width }) => { - const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ - showTimeline, - timelineId, - ]); - const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ - showTimeline, - timelineId, - ]); - - return ( - <> - - - - - - - - ); - } -); - -FlyoutComponent.displayName = 'FlyoutComponent'; - const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; const DEFAULT_TIMELINE_BY_ID = {}; -const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById: TimelineById = - timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; - /* - In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender - of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS - */ - const dataProviders = timelineById[timelineId]?.dataProviders.length - ? timelineById[timelineId]?.dataProviders - : DEFAULT_DATA_PROVIDERS; - const show = timelineById[timelineId]?.show ?? false; - const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; - - return { dataProviders, show, width }; -}; - -const mapDispatchToProps = { - showTimeline: timelineActions.showTimeline, +const FlyoutComponent: React.FC = ({ timelineId, usersViewing }) => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const dispatch = useDispatch(); + const { dataProviders = DEFAULT_DATA_PROVIDERS, show = false } = useDeepEqualSelector( + (state) => getTimeline(state, timelineId) ?? DEFAULT_TIMELINE_BY_ID + ); + const handleClose = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), + [dispatch, timelineId] + ); + const handleOpen = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })), + [dispatch, timelineId] + ); + + return ( + <> + + + + + + ); }; -const connector = connect(mapStateToProps, mapDispatchToProps); - -type ProsFromRedux = ConnectedProps; +FlyoutComponent.displayName = 'FlyoutComponent'; -export const Flyout = connector(FlyoutComponent); +export const Flyout = React.memo(FlyoutComponent); Flyout.displayName = 'Flyout'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 7528468ef6522..10eb140515826 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -5,113 +5,48 @@ */ import { EuiFlyout } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import styled from 'styled-components'; -import { Resizable, ResizeCallback } from 're-resizable'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; -import { useFullScreen } from '../../../../common/containers/use_full_screen'; -import { timelineActions } from '../../../store/timeline'; - -import { TimelineResizeHandle } from './timeline_resize_handle'; - +import { StatefulTimeline } from '../../timeline'; import * as i18n from './translations'; -const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) -const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view interface FlyoutPaneComponentProps { - children: React.ReactNode; onClose: () => void; timelineId: string; - width: number; + usersViewing: string[]; } const EuiFlyoutContainer = styled.div` .timeline-flyout { z-index: 4001; min-width: 150px; - width: auto; + width: 100%; animation: none; } `; -const StyledResizable = styled(Resizable)` - display: flex; - flex-direction: column; -`; - -const RESIZABLE_ENABLE = { left: true }; - -const RESIZABLE_DISABLED = { left: false }; - const FlyoutPaneComponent: React.FC = ({ - children, onClose, timelineId, - width, -}) => { - const dispatch = useDispatch(); - const { timelineFullScreen } = useFullScreen(); - - const onResizeStop: ResizeCallback = useCallback( - (_e, _direction, _ref, delta) => { - const bodyClientWidthPixels = document.body.clientWidth; - - if (delta.width) { - dispatch( - timelineActions.applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -delta.width, - id: timelineId, - maxWidthPercent, - minWidthPixels, - }) - ); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dispatch] - ); - const resizableDefaultSize = useMemo( - () => ({ - width, - height: '100%', - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const resizableHandleComponent = useMemo( - () => ({ - left: , - }), - [] - ); - - return ( - - - - {children} - - - - ); -}; + usersViewing, +}) => ( + + + + + + + +); export const Pane = React.memo(FlyoutPaneComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 576dedfc28b1b..6fddb5403561e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -20,5 +20,3 @@ export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px - -export const DEFAULT_TIMELINE_WIDTH = 1100; // px diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index ce469c2bf57a2..37c6da93725b9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -7,7 +7,6 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { Direction } from '../../../graphql/types'; -import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { SubsetTimelineModel, TimelineModel } from './model'; @@ -57,6 +56,5 @@ export const timelineDefaults: SubsetTimelineModel & Pick Date: Thu, 29 Oct 2020 16:27:48 +0100 Subject: [PATCH 02/58] cleanup --- .../event_details/event_details.tsx | 6 - .../event_details/stateful_event_details.tsx | 17 +- .../timeline/body/actions/index.tsx | 26 +- .../body/events/event_column_view.tsx | 9 - .../components/timeline/body/events/index.tsx | 27 +- .../timeline/body/events/stateful_event.tsx | 275 ++++-------------- .../components/timeline/body/index.tsx | 172 +++++++---- .../timeline/expandable_event/index.tsx | 23 +- .../__snapshots__/index.test.tsx.snap | 18 -- .../timeline/skeleton_row/index.test.tsx | 45 --- .../timeline/skeleton_row/index.tsx | 77 ----- 11 files changed, 192 insertions(+), 503 deletions(-) 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/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 074e6faf80c7d..a930d20fb1caa 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 @@ -15,7 +15,6 @@ 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'; @@ -31,7 +30,6 @@ interface Props { data: TimelineEventsDetailsItem[]; id: string; view: View; - onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: View) => void; timelineId: string; @@ -51,7 +49,6 @@ export const EventDetails = React.memo( data, id, view, - onEventToggled, onUpdateColumns, onViewSelected, timelineId, @@ -90,9 +87,6 @@ export const EventDetails = React.memo( selectedTab={view === 'table-view' ? tabs[0] : tabs[1]} onTabClick={(e) => onViewSelected(e.id as View)} /> - - {COLLAPSE_EVENT} - ); } 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..f572a5910bb4a 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 @@ -18,35 +18,24 @@ interface Props { 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, - }) => { + ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + // TODO: Move to the store const [view, setView] = useState('table-view'); - const handleSetView = useCallback((newView) => setView(newView), []); return ( ; - onEventToggled: () => void; showCheckboxes: boolean; } @@ -28,11 +24,8 @@ const ActionsComponent: React.FC = ({ actionsColumnWidth, additionalActions, checked, - expanded, eventId, - loading = false, loadingEventIds, - onEventToggled, onRowSelected, showCheckboxes, }) => { @@ -66,21 +59,6 @@ const ActionsComponent: React.FC = ({ )} - - - {loading ? ( - - ) : ( - - )} - - <>{additionalActions} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6d4325f00739..c3047c5e7d9f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -42,14 +42,11 @@ interface Props { data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; - expanded: boolean; getNotesByIds: (noteIds: string[]) => Note[]; isEventPinned: boolean; isEventViewer?: boolean; - loading: boolean; loadingEventIds: Readonly; onColumnResized: OnColumnResized; - onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; @@ -77,14 +74,11 @@ export const EventColumnView = React.memo( data, ecsData, eventIdToNoteIds, - expanded, getNotesByIds, isEventPinned = false, isEventViewer = false, - loading, loadingEventIds, onColumnResized, - onEventToggled, onPinEvent, onRowSelected, onUnPinEvent, @@ -191,12 +185,9 @@ export const EventColumnView = React.memo( additionalActions={additionalActions} checked={Object.keys(selectedEventIds).includes(id)} onRowSelected={onRowSelected} - expanded={expanded} data-test-subj="actions" eventId={id} - loading={loading} loadingEventIds={loadingEventIds} - onEventToggled={onEventToggled} showCheckboxes={showCheckboxes} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index 752e0bdf25230..79a03d526dd8e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { inputsModel } from '../../../../../common/store'; -import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; +import { BrowserFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData, @@ -15,13 +15,7 @@ import { import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { Note } from '../../../../../common/lib/note'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { - OnColumnResized, - OnPinEvent, - OnRowSelected, - OnUnPinEvent, - OnUpdateColumns, -} from '../../events'; +import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTbody } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; @@ -34,18 +28,17 @@ interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; - containerElementRef: HTMLDivElement; data: TimelineItem[]; - docValueFields: DocValueFields[]; + expanded: { eventId?: string; indexName?: string }; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; isEventViewer?: boolean; loadingEventIds: Readonly; onColumnResized: OnColumnResized; + onEventToggled: (event: TimelineItem) => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; @@ -63,9 +56,7 @@ const EventsComponent: React.FC = ({ browserFields, columnHeaders, columnRenderers, - containerElementRef, data, - docValueFields, eventIdToNoteIds, getNotesByIds, id, @@ -74,15 +65,14 @@ const EventsComponent: React.FC = ({ onColumnResized, onPinEvent, onRowSelected, - onUpdateColumns, onUnPinEvent, pinnedEventIds, refetch, + onEventToggled, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, - toggleColumn, updateNote, }) => ( @@ -93,9 +83,6 @@ const EventsComponent: React.FC = ({ browserFields={browserFields} columnHeaders={columnHeaders} columnRenderers={columnRenderers} - containerElementRef={containerElementRef} - disableSensorVisibility={data != null && data.length < 101} - docValueFields={docValueFields} event={event} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} @@ -107,14 +94,14 @@ const EventsComponent: React.FC = ({ onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} - onUpdateColumns={onUpdateColumns} refetch={refetch} rowRenderers={rowRenderers} + // TODO: Move to the store + onEventToggled={() => onEventToggled(event)} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} timelineId={id} - toggleColumn={toggleColumn} updateNote={updateNote} /> ))} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4f385a4656483..89698ddb2e0a9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -6,28 +6,17 @@ import React, { useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; -import VisibilitySensor from 'react-visibility-sensor'; -import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; +import { BrowserFields } from '../../../../../common/containers/source'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useTimelineEventsDetails } from '../../../../containers/details'; import { - TimelineEventsDetailsItem, TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { SkeletonRow } from '../../skeleton_row'; -import { - OnColumnResized, - OnPinEvent, - OnRowSelected, - OnUnPinEvent, - OnUpdateColumns, -} from '../../events'; -import { ExpandableEvent } from '../../expandable_event'; +import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; @@ -41,23 +30,20 @@ import { inputsModel } from '../../../../../common/store'; interface Props { actionsColumnWidth: number; - containerElementRef: HTMLDivElement; addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; - disableSensorVisibility: boolean; - docValueFields: DocValueFields[]; event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; isEventViewer?: boolean; loadingEventIds: Readonly; onColumnResized: OnColumnResized; + onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; - onUpdateColumns: OnUpdateColumns; isEventPinned: boolean; refetch: inputsModel.Refetch; onRuleChange?: () => void; @@ -65,41 +51,11 @@ interface Props { selectedEventIds: Readonly>; showCheckboxes: boolean; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } export const getNewNoteId = (): string => uuid.v4(); -const emptyDetails: TimelineEventsDetailsItem[] = []; - -/** - * This is the default row height whenever it is a plain row renderer and not a custom row height. - * We use this value when we do not know the height of a particular row. - */ -const DEFAULT_ROW_HEIGHT = '32px'; - -/** - * This is the top offset in pixels of the top part of the timeline. The UI area where you do your - * drag and drop and filtering. It is a positive number in pixels of _PART_ of the header but not - * the entire header. We leave room for some rows to render behind the drag and drop so they might be - * visible by the time the user scrolls upwards. All other DOM elements are replaced with their "blank" - * rows. - */ -const TOP_OFFSET = 50; - -/** - * This is the bottom offset in pixels of the bottom part of the timeline. The UI area right below the - * timeline which is the footer. Since the footer is so incredibly small we don't have enough room to - * render around 5 rows below the timeline to get the user the best chance of always scrolling without seeing - * "blank rows". The negative number is to give the bottom of the browser window a bit of invisible space to - * keep around 5 rows rendering below it. All other DOM elements are replaced with their "blank" - * rows. - */ -const BOTTOM_OFFSET = -500; - -const VISIBILITY_SENSOR_OFFSET = { top: TOP_OFFSET, bottom: BOTTOM_OFFSET }; - const emptyNotes: string[] = []; const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { @@ -113,11 +69,8 @@ const StatefulEventComponent: React.FC = ({ actionsColumnWidth, addNoteToEvent, browserFields, - containerElementRef, columnHeaders, columnRenderers, - disableSensorVisibility = true, - docValueFields, event, eventIdToNoteIds, getNotesByIds, @@ -125,45 +78,29 @@ const StatefulEventComponent: React.FC = ({ isEventPinned = false, loadingEventIds, onColumnResized, + onEventToggled, onPinEvent, onRowSelected, onUnPinEvent, - onUpdateColumns, refetch, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, timelineId, - toggleColumn, updateNote, }) => { - const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); const { status: timelineStatus } = useShallowEqualSelector( (state) => state.timeline.timelineById[timelineId] ); const divElement = useRef(null); - const [loading, detailsData] = useTimelineEventsDetails({ - docValueFields, - indexName: event._index!, - eventId: event._id, - skip: !expanded[event._id], - }); const onToggleShowNotes = useCallback(() => { const eventId = event._id; setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); }, [event, showNotes]); - const onToggleExpanded = useCallback(() => { - const eventId = event._id; - setExpanded({ - ...expanded, - [eventId]: !expanded[eventId], - }); - }, [event, expanded]); - const associateNote = useCallback( (noteId: string) => { addNoteToEvent({ eventId: event._id, noteId }); @@ -174,152 +111,68 @@ const StatefulEventComponent: React.FC = ({ [addNoteToEvent, event, isEventPinned, onPinEvent] ); - // Number of current columns plus one for actions. - const columnCount = columnHeaders.length + 1; - - const VisibilitySensorContent = useCallback( - ({ isVisible }) => { - if (isVisible || disableSensorVisibility) { - return ( - - - - - - - - - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - timelineId, - })} - - - - - - - ); - } else { - // Height place holder for visibility detection as well as re-rendering sections. - const height = - divElement.current != null && divElement.current!.clientHeight - ? `${divElement.current!.clientHeight}px` - : DEFAULT_ROW_HEIGHT; - - return ; - } - }, - [ - actionsColumnWidth, - associateNote, - browserFields, - columnCount, - columnHeaders, - columnRenderers, - detailsData, - disableSensorVisibility, - event._id, - event.data, - event.ecs, - eventIdToNoteIds, - expanded, - getNotesByIds, - isEventPinned, - isEventViewer, - loading, - loadingEventIds, - onColumnResized, - onPinEvent, - onRowSelected, - onToggleExpanded, - onToggleShowNotes, - onUnPinEvent, - onUpdateColumns, - refetch, - onRuleChange, - rowRenderers, - selectedEventIds, - showCheckboxes, - showNotes, - timelineId, - timelineStatus, - toggleColumn, - updateNote, - ] - ); - return ( - - {VisibilitySensorContent} - + + + + + + + + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index e1667ab949732..b42124fa4d1e4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useRef } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; -import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { + TimelineItem, + TimelineEventsDetailsItem, + TimelineNonEcsData, +} from '../../../../../common/search_strategy'; import { Note } from '../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; @@ -32,6 +38,8 @@ import { Sort } from './sort'; import { GraphOverlay } from '../../graph_overlay'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { TimelineEventsType, TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { ExpandableEvent } from '../expandable_event'; +import { useTimelineEventsDetails } from '../../../containers/details'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -76,6 +84,16 @@ export const hasAdditionalActions = (id: TimelineId): boolean => const EXTRA_WIDTH = 4; // px +const emptyDetails: TimelineEventsDetailsItem[] = []; + +const FullWithFlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + /** Renders the timeline body */ export const Body = React.memo( ({ @@ -112,7 +130,26 @@ export const Body = React.memo( timelineType, updateNote, }) => { - const containerElementRef = useRef(null); + const [expanded, setExpanded] = useState<{ eventId?: string; indexName?: string }>({}); + const [loading, detailsData] = useTimelineEventsDetails({ + docValueFields, + indexName: expanded.indexName!, + eventId: expanded.eventId!, + skip: !expanded.eventId, + }); + + const onEventToggled = useCallback((event) => { + const eventId = event._id; + + setExpanded((currentExpanded) => { + if (currentExpanded.eventId === eventId) { + return {}; + } + + return { eventId, indexName: event._index }; + }); + }, []); + const actionsColumnWidth = useMemo( () => getActionsColumnWidth( @@ -141,64 +178,81 @@ export const Body = React.memo( timelineType={timelineType} /> )} - - - - - - - + + +
+ + + + + + + +
+
+ + {expanded.eventId && ( + + )} + +
); } ); -Body.displayName = 'Body'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index b1f48608346c7..d0258c2027a96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -14,15 +14,10 @@ import { StatefulEventDetails } from '../../../../common/components/event_detail import { LazyAccordion } from '../../lazy_accordion'; import { OnUpdateColumns } from '../events'; -const ExpandableDetails = styled.div<{ hideExpandButton: boolean }>` - ${({ hideExpandButton }) => - hideExpandButton - ? ` +const ExpandableDetails = styled.div` .euiAccordion__button { display: none; } - ` - : ''}; `; ExpandableDetails.displayName = 'ExpandableDetails'; @@ -34,7 +29,6 @@ interface Props { event: TimelineEventsDetailsItem[]; forceExpand?: boolean; hideExpandButton?: boolean; - onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; @@ -49,7 +43,6 @@ export const ExpandableEvent = React.memo( id, timelineId, toggleColumn, - onEventToggled, onUpdateColumns, }) => { const handleRenderExpandedContent = useCallback( @@ -59,26 +52,16 @@ export const ExpandableEvent = React.memo( columnHeaders={columnHeaders} data={event} id={id} - onEventToggled={onEventToggled} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} /> ), - [ - browserFields, - columnHeaders, - event, - id, - onEventToggled, - onUpdateColumns, - timelineId, - toggleColumn, - ] + [browserFields, columnHeaders, event, id, onUpdateColumns, timelineId, toggleColumn] ); return ( - + - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx deleted file mode 100644 index b63359077bf2c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.test.tsx +++ /dev/null @@ -1,45 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../../common/mock'; -import { SkeletonRow } from './index'; - -describe('SkeletonRow', () => { - test('it renders', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the correct number of cells if cellCount is specified', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); - }); - - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { - const wrapper = mount( - - - - ); - const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); - const siemSkeletonRowCell = wrapper.find('.siemSkeletonRow__cell').last(); - - expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); - expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); - expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { - modifier: '& + &', - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx deleted file mode 100644 index ae30f11d8bb16..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/skeleton_row/index.tsx +++ /dev/null @@ -1,77 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import styled from 'styled-components'; - -interface RowProps { - rowHeight?: string; - rowPadding?: string; -} - -const RowComponent = styled.div.attrs(({ rowHeight, rowPadding, theme }) => ({ - className: 'siemSkeletonRow', - rowHeight: rowHeight || theme.eui.euiSizeXL, - rowPadding: rowPadding || `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.xs}`, -}))` - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - display: flex; - height: ${({ rowHeight }) => rowHeight}; - padding: ${({ rowPadding }) => rowPadding}; -`; -RowComponent.displayName = 'RowComponent'; - -const Row = React.memo(RowComponent); - -Row.displayName = 'Row'; - -interface CellProps { - cellColor?: string; - cellMargin?: string; -} - -const CellComponent = styled.div.attrs(({ cellColor, cellMargin, theme }) => ({ - className: 'siemSkeletonRow__cell', - cellColor: cellColor || theme.eui.euiColorLightestShade, - cellMargin: cellMargin || theme.eui.gutterTypes.gutterSmall, -}))` - background-color: ${({ cellColor }) => cellColor}; - border-radius: 2px; - flex: 1; - - & + & { - margin-left: ${({ cellMargin }) => cellMargin}; - } -`; -CellComponent.displayName = 'CellComponent'; - -const Cell = React.memo(CellComponent); - -Cell.displayName = 'Cell'; - -export interface SkeletonRowProps extends CellProps, RowProps { - cellCount?: number; -} - -export const SkeletonRow = React.memo( - ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding }) => { - const cells = useMemo( - () => - [...Array(cellCount)].map( - (_, i) => , - [cellCount] - ), - [cellCount, cellColor, cellMargin] - ); - - return ( - - {cells} - - ); - } -); -SkeletonRow.displayName = 'SkeletonRow'; From b19570e6a1770680d03faa1a1c598f90fe2c6092 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 3 Nov 2020 09:10:53 +0100 Subject: [PATCH 03/58] add styling --- .../components/event_details/columns.tsx | 29 +++++++++++-------- .../event_details/event_details.tsx | 6 ++-- .../components/timeline/body/events/index.tsx | 2 ++ .../timeline/body/events/stateful_event.tsx | 6 ++-- .../components/timeline/body/index.tsx | 7 +++-- .../timelines/components/timeline/styles.tsx | 16 ++++++++-- 6 files changed, 45 insertions(+), 21 deletions(-) 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..90dfb4201e1ab 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 @@ -14,6 +14,7 @@ import { EuiPanel, EuiText, EuiToolTip, + EuiIconTip, } from '@elastic/eui'; import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; @@ -90,6 +91,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 +203,7 @@ 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.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index a930d20fb1caa..58fa6cdaeadd8 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'; @@ -54,6 +54,8 @@ export const EventDetails = React.memo( timelineId, toggleColumn, }) => { + const handleTabClick = useCallback((e) => onViewSelected(e.id as View), [onViewSelected]); + const tabs: EuiTabbedContentTab[] = useMemo( () => [ { @@ -85,7 +87,7 @@ export const EventDetails = React.memo( onViewSelected(e.id as View)} + onTabClick={handleTabClick} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index 79a03d526dd8e..71de3f4eba76d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -58,6 +58,7 @@ const EventsComponent: React.FC = ({ columnRenderers, data, eventIdToNoteIds, + expanded, getNotesByIds, id, isEventViewer = false, @@ -88,6 +89,7 @@ const EventsComponent: React.FC = ({ getNotesByIds={getNotesByIds} isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} + isExpanded={expanded.eventId === event._id} key={`${event._id}_${event._index}`} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 89698ddb2e0a9..5f5799f83b817 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,7 +8,7 @@ import React, { useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; import { BrowserFields } from '../../../../../common/containers/source'; -import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineItem, TimelineNonEcsData, @@ -76,6 +76,7 @@ const StatefulEventComponent: React.FC = ({ getNotesByIds, isEventViewer = false, isEventPinned = false, + isExpanded = false, loadingEventIds, onColumnResized, onEventToggled, @@ -91,7 +92,7 @@ const StatefulEventComponent: React.FC = ({ updateNote, }) => { const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const { status: timelineStatus } = useShallowEqualSelector( + const { status: timelineStatus } = useDeepEqualSelector( (state) => state.timeline.timelineById[timelineId] ); const divElement = useRef(null); @@ -117,6 +118,7 @@ const StatefulEventComponent: React.FC = ({ data-test-subj="event" eventType={getEventType(event.ecs)} isBuildingBlockType={isEventBuildingBlockType(event.ecs)} + isExpanded={isExpanded} showLeftBorder={!isEventViewer} ref={divElement} onClick={onEventToggled} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index b42124fa4d1e4..c99cdbc81d59c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -86,8 +86,9 @@ const EXTRA_WIDTH = 4; // px const emptyDetails: TimelineEventsDetailsItem[] = []; -const FullWithFlexGroup = styled(EuiFlexGroup)` +const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; + overflow: hidden; `; const ScrollableFlexItem = styled(EuiFlexItem)` @@ -178,7 +179,7 @@ export const Body = React.memo( timelineType={timelineType} /> )} - +
( /> )} - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index d146818e7ab90..0ac1fb7153827 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -176,17 +176,19 @@ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ }))<{ className?: string; eventType: Omit; + isExpanded: boolean; isBuildingBlockType: boolean; showLeftBorder: boolean; }>` + cursor: pointer; border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorLightShade}; - ${({ theme, eventType, isBuildingBlockType, showLeftBorder }) => + ${({ theme, eventType, showLeftBorder }) => showLeftBorder ? `border-left: 4px solid ${eventType === 'raw' ? theme.eui.euiColorLightShade : theme.eui.euiColorWarning}` : ''}; - ${({ isBuildingBlockType, showLeftBorder }) => + ${({ isBuildingBlockType }) => isBuildingBlockType ? `background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);` : ''}; @@ -194,6 +196,16 @@ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ &:hover { background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; } + + ${({ isExpanded, theme }) => + isExpanded && + ` + background: ${theme.eui.euiTableSelectedColor}; + + &:hover { + ${theme.eui.euiTableHoverSelectedColor} + } + `} `; export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ From c35c70ab767396013fa15965f659966ce1b9e9ba Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 3 Nov 2020 10:40:12 +0100 Subject: [PATCH 04/58] fix styling --- .../components/timeline/body/index.tsx | 113 +++++++++--------- .../components/timeline/body/translations.ts | 7 ++ 2 files changed, 64 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index c99cdbc81d59c..cd89aabae78cd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -40,6 +40,7 @@ import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { TimelineEventsType, TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { ExpandableEvent } from '../expandable_event'; import { useTimelineEventsDetails } from '../../../containers/details'; +import * as i18n from './translations'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -181,64 +182,62 @@ export const Body = React.memo( )} -
- - - + + + - - - -
+ + +
- {expanded.eventId && ( + {expanded.eventId ? ( ( timelineId={timelineId} toggleColumn={toggleColumn} /> + ) : ( + {i18n.EVENT_DETAILS_PLACEHOLDER} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index c57002023b79d..f7e281c8c6db3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -93,3 +93,10 @@ export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( defaultMessage: 'Analyze event', } ); + +export const EVENT_DETAILS_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.timeline.eventDetailsPlaceholder', + { + defaultMessage: 'Select an event to show event details', + } +); From 3fa7ac142776bd07f1c7c1ec016b16d660e2d2b0 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Sun, 8 Nov 2020 19:51:08 +0100 Subject: [PATCH 05/58] fix types --- .../public/timelines/components/flyout/pane/index.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx index 1d0cdc4c30b7b..fed6a39ae2ed5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx @@ -10,8 +10,6 @@ import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { Pane } from '.'; -const testWidth = 640; - describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( From 09f3d9f91ece8470aca7c26cfd29b2416e773fee Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Sun, 8 Nov 2020 23:29:32 +0100 Subject: [PATCH 06/58] update cypress --- x-pack/plugins/security_solution/cypress/screens/alerts.ts | 2 +- x-pack/plugins/security_solution/cypress/screens/timeline.ts | 2 +- .../public/detections/components/alerts_table/actions.test.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index ed05874bd4c4d..c5f6218ba2617 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -28,7 +28,7 @@ export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsBu export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; -export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; +export const EXPAND_ALERT_BTN = '[data-test-subj="event-actions-container"]'; export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 98e6502ffe94f..5682f73532966 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -127,4 +127,4 @@ export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"] export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; -export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; +export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="event-actions-container"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index ecc0fc54d0d47..db8b63bd23eb1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -253,7 +253,6 @@ describe('alert actions', () => { templateTimelineId: null, templateTimelineVersion: null, version: null, - width: 1100, }, to: '2018-11-05T19:03:25.937Z', ruleNote: '# this is some markdown documentation', From 0f9291038809e7ea72432b6af568bdc73e06408f Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Sun, 8 Nov 2020 23:41:18 +0100 Subject: [PATCH 07/58] update tests --- .../cypress/integration/timeline_toggle_column.spec.ts | 4 ++++ .../plugins/security_solution/cypress/tasks/security_main.ts | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index e4f303fb89fda..4a9dbfe3960a0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -41,6 +41,7 @@ describe('toggle column in timeline', () => { it('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { expandFirstTimelineEventDetails(); cy.get(TIMESTAMP_TOGGLE_FIELD).should('be.checked'); + expandFirstTimelineEventDetails(); }); it('displays an Unchecked Toggle field checkbox for `_id`, because it is NOT a default timeline column', () => { @@ -52,6 +53,7 @@ describe('toggle column in timeline', () => { uncheckTimestampToggleField(); cy.get(TIMESTAMP_HEADER_FIELD).should('not.exist'); + expandFirstTimelineEventDetails(); }); it('adds the _id field to the timeline when the user checks the field', () => { @@ -59,6 +61,7 @@ describe('toggle column in timeline', () => { checkIdToggleField(); cy.get(ID_HEADER_FIELD).should('exist'); + expandFirstTimelineEventDetails(); }); it('adds the _id field to the timeline via drag and drop', () => { @@ -66,5 +69,6 @@ describe('toggle column in timeline', () => { dragAndDropIdToggleFieldToTimeline(); cy.get(ID_HEADER_FIELD).should('exist'); + expandFirstTimelineEventDetails(); }); }); 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(); } }); -}; From 3c86b0f9295ec6a15435862b9501f9e746b06010 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Mon, 9 Nov 2020 13:09:43 +0100 Subject: [PATCH 08/58] fix styling --- .../events_viewer/events_viewer.tsx | 52 ++++++++-------- .../components/timeline/body/index.tsx | 14 +---- .../components/timeline/timeline.tsx | 59 ++++++++++--------- 3 files changed, 61 insertions(+), 64 deletions(-) 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 42ebd896f3ac5..861661daa3913 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,9 +36,10 @@ 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 { ActiveTimelineExpandedEvent } from '../../../timelines/containers/active_timeline_context'; import { ExpandableEvent } from '../../../timelines/components/timeline/expandable_event'; +import { GraphOverlay } from '../../../timelines/components/graph_overlay'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -78,9 +79,10 @@ const EventsContainerLoading = styled.div` flex-direction: column; `; -const FullWidthFlexGroup = styled(EuiFlexGroup)` +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` width: 100%; overflow: hidden; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; `; const ScrollableFlexItem = styled(EuiFlexItem)` @@ -305,7 +307,15 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - + {graphEventId && ( + + )} + = ({ sort={sort} toggleColumn={toggleColumn} /> - - { - /** Hide the footer if Resolver is showing. */ - !graphEventId && ( -
- ) - } +
void; updateNote: UpdateNote; } @@ -113,7 +111,6 @@ export const Body = React.memo( sort, toggleColumn, timelineId, - timelineType, updateNote, }) => { const actionsColumnWidth = useMemo( @@ -136,15 +133,6 @@ export const Body = React.memo( return ( <> - {graphEventId && ( - - )} - ` width: 100%; overflow: hidden; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; `; const ScrollableFlexItem = styled(EuiFlexItem)` @@ -299,7 +301,15 @@ export const TimelineComponent: React.FC = ({ loading={loading} refetch={refetch} /> - + {graphEventId && ( + + )} + = ({ toggleColumn={toggleColumn} /> - { - /** Hide the footer if Resolver is showing. */ - !graphEventId && ( - -
- - ) - } + +
+ Date: Tue, 10 Nov 2020 17:04:15 +0100 Subject: [PATCH 09/58] event details flyout --- .../cypress/screens/alerts.ts | 2 +- .../cypress/screens/timeline.ts | 2 +- .../components/event_details/columns.tsx | 1 - .../events_viewer/events_viewer.tsx | 182 ++++++++++-------- .../components/events_viewer/translations.ts | 7 + .../timeline/body/actions/index.test.tsx | 4 + .../timeline/body/actions/index.tsx | 18 +- .../body/events/event_column_view.tsx | 6 + .../timeline/body/events/stateful_event.tsx | 3 +- .../components/timeline/body/index.test.tsx | 2 - .../timeline/body/stateful_body.tsx | 7 +- .../timelines/components/timeline/styles.tsx | 1 - .../components/timeline/timeline.test.tsx | 14 -- 13 files changed, 142 insertions(+), 107 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index c5f6218ba2617..ed05874bd4c4d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -28,7 +28,7 @@ export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsBu export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; -export const EXPAND_ALERT_BTN = '[data-test-subj="event-actions-container"]'; +export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 5682f73532966..98e6502ffe94f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -127,4 +127,4 @@ export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"] export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; -export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="event-actions-container"]'; +export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-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 3eec9414993f6..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 @@ -201,7 +201,6 @@ export const getColumns = ({ ), }, - { field: 'valuesConcatenated', name: i18n.BLANK, 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 861661daa3913..92820c9c52b25 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 @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -97,6 +105,10 @@ const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` ${({ show }) => (show ? '' : 'visibility: hidden;')} `; +const EventDetailsFlyout = styled(EuiFlyout)` + z-index: 9999; +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -156,7 +168,6 @@ const EventsViewerComponent: React.FC = ({ const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const [isQueryLoading, setIsQueryLoading] = useState(false); - const [expanded, setExpanded] = useState({}); const onEventToggled = useCallback((event: TimelineItem) => { @@ -171,6 +182,8 @@ const EventsViewerComponent: React.FC = ({ }); }, []); + const handleClearSelection = useCallback(() => setExpanded({}), []); + const { getManageTimelineById, setIsTimelineLoading } = useManageTimeline(); useEffect(() => { @@ -280,87 +293,98 @@ const EventsViewerComponent: React.FC = ({ }, [loading]); return ( - - {canQueryTimeline ? ( - - <> - - {HeaderSectionContent} - - {utilityBar && !resolverIsShowing(graphEventId) && ( - {utilityBar?.(refetch, totalCountMinusDeleted)} - )} - - - - {graphEventId && ( - + <> + + {canQueryTimeline ? ( + + <> + + {HeaderSectionContent} + + {utilityBar && !resolverIsShowing(graphEventId) && ( + {utilityBar?.(refetch, totalCountMinusDeleted)} )} - - - + + + {graphEventId && ( + -
- - - - - - - - - ) : null} - + )} + + + +
+ + + + + + ) : null} + + {expanded.eventId && ( + + + +

{i18n.EVENT_DETAILS}

+
+
+ + + +
+ )} + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts index 9e553b8ca9d4f..4d0128e586d54 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts @@ -21,6 +21,13 @@ export const EVENTS = i18n.translate('xpack.securitySolution.eventsViewer.events defaultMessage: 'Events', }); +export const EVENT_DETAILS = i18n.translate( + 'xpack.securitySolution.eventDetailsFlyout.headerTitle', + { + defaultMessage: 'Event details', + } +); + export const LOADING_EVENTS = i18n.translate( 'xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 5ac01baec69d8..a728e35122060 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -26,8 +26,10 @@ describe('Actions', () => { @@ -44,7 +46,9 @@ describe('Actions', () => { actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} checked={false} eventId="abc" + expanded={false} loadingEventIds={[]} + onEventToggled={jest.fn()} onRowSelected={jest.fn()} showCheckboxes={false} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 548d5793ef9e3..88eb1721d60db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; -import { EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; +import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; import { EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; +import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; @@ -15,8 +16,10 @@ interface Props { additionalActions?: JSX.Element[]; checked: boolean; onRowSelected: OnRowSelected; + expanded: boolean; eventId: string; loadingEventIds: Readonly; + onEventToggled: () => void; showCheckboxes: boolean; } @@ -24,8 +27,10 @@ const ActionsComponent: React.FC = ({ actionsColumnWidth, additionalActions, checked, + expanded, eventId, loadingEventIds, + onEventToggled, onRowSelected, showCheckboxes, }) => { @@ -59,6 +64,17 @@ const ActionsComponent: React.FC = ({ )} + + + + + <>{additionalActions} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c3047c5e7d9f3..15d7d750257ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -42,11 +42,13 @@ interface Props { data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; + expanded: boolean; getNotesByIds: (noteIds: string[]) => Note[]; isEventPinned: boolean; isEventViewer?: boolean; loadingEventIds: Readonly; onColumnResized: OnColumnResized; + onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; @@ -74,11 +76,13 @@ export const EventColumnView = React.memo( data, ecsData, eventIdToNoteIds, + expanded, getNotesByIds, isEventPinned = false, isEventViewer = false, loadingEventIds, onColumnResized, + onEventToggled, onPinEvent, onRowSelected, onUnPinEvent, @@ -185,9 +189,11 @@ export const EventColumnView = React.memo( additionalActions={additionalActions} checked={Object.keys(selectedEventIds).includes(id)} onRowSelected={onRowSelected} + expanded={expanded} data-test-subj="actions" eventId={id} loadingEventIds={loadingEventIds} + onEventToggled={onEventToggled} showCheckboxes={showCheckboxes} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index bef4a9cb70d85..0d2d43b2d7399 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -122,7 +122,6 @@ const StatefulEventComponent: React.FC = ({ isExpanded={isExpanded} showLeftBorder={!isEventViewer} ref={divElement} - onClick={onEventToggled} > = ({ data={event.data} ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} + expanded={isExpanded} getNotesByIds={getNotesByIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} + onEventToggled={onEventToggled} onPinEvent={onPinEvent} onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index c43300262f9c5..2186cac42597b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -18,7 +18,6 @@ import { Sort } from './sort'; import { waitFor } from '@testing-library/react'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; -import { TimelineType } from '../../../../../common/types/timeline'; const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { @@ -80,7 +79,6 @@ describe('Body', () => { sort: mockSort, showCheckboxes: false, timelineId: 'timeline-test', - timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 8b8be95f9798c..3e49e1f87f791 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -85,7 +85,6 @@ const StatefulBodyComponent = React.memo( graphEventId, refetch, sort, - timelineType, toggleColumn, unPinEvent, updateColumns, @@ -227,7 +226,6 @@ const StatefulBodyComponent = React.memo( showCheckboxes={showCheckboxes} sort={sort} timelineId={id} - timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} /> @@ -252,8 +250,7 @@ const StatefulBodyComponent = React.memo( prevProps.show === nextProps.show && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.sort === nextProps.sort && - prevProps.timelineType === nextProps.timelineType + prevProps.sort === nextProps.sort ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; @@ -279,7 +276,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - timelineType, } = timeline; return { @@ -295,7 +291,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - timelineType, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 0ac1fb7153827..e4c49ce197c2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -180,7 +180,6 @@ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ isBuildingBlockType: boolean; showLeftBorder: boolean; }>` - cursor: pointer; border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorLightShade}; ${({ theme, eventType, showLeftBorder }) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 7fc269c954ac4..900699503a3bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -214,19 +214,5 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timeline-footer"]').exists()).toEqual(true); }); - describe('when there is a graphEventId', () => { - beforeEach(() => { - props.graphEventId = 'graphEventId'; // any string w/ length > 0 works - }); - it('should not show the timeline footer', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-footer"]').exists()).toEqual(false); - }); - }); }); }); From d6c0d4d6a092dcbee19aafd05c41d45e1f8dcf47 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Fri, 13 Nov 2020 09:31:02 +0100 Subject: [PATCH 10/58] WIP --- .../event_details/event_details.tsx | 5 +- .../event_fields_browser.test.tsx | 9 - .../event_details/event_fields_browser.tsx | 34 +- .../event_details/stateful_event_details.tsx | 4 +- .../events_viewer/events_viewer.test.tsx | 1 - .../events_viewer/events_viewer.tsx | 4 - .../common/components/events_viewer/index.tsx | 27 -- .../fields_browser/field_browser.test.tsx | 7 - .../fields_browser/field_browser.tsx | 6 - .../fields_browser/fields_pane.test.tsx | 4 - .../components/fields_browser/fields_pane.tsx | 102 +++--- .../components/fields_browser/index.test.tsx | 7 - .../components/fields_browser/index.tsx | 2 - .../components/fields_browser/types.ts | 2 - .../components/flyout/header/index.tsx | 127 +++++--- .../flyout/header_with_close_button/index.tsx | 1 + .../components/notes/add_note/index.test.tsx | 21 +- .../components/notes/add_note/index.tsx | 25 +- .../timelines/components/notes/helpers.tsx | 17 +- .../timelines/components/notes/index.tsx | 104 +++++-- .../notes/note_cards/index.test.tsx | 1 - .../components/notes/note_cards/index.tsx | 30 +- .../components/open_timeline/helpers.ts | 3 +- .../body/actions/add_note_icon_item.tsx | 9 +- .../body/column_headers/index.test.tsx | 3 - .../timeline/body/column_headers/index.tsx | 4 - .../body/data_driven_columns/index.tsx | 2 - .../body/events/event_column_view.test.tsx | 1 - .../body/events/event_column_view.tsx | 16 +- .../components/timeline/body/events/index.tsx | 23 +- .../timeline/body/events/stateful_event.tsx | 47 +-- .../components/timeline/body/index.test.tsx | 7 - .../components/timeline/body/index.tsx | 24 -- .../timeline/body/stateful_body.tsx | 52 +--- .../timeline/expandable_event/index.tsx | 15 +- .../timelines/components/timeline/index.tsx | 90 +++--- .../timeline/properties/helpers.tsx | 65 +--- .../timeline/properties/index.test.tsx | 1 - .../components/timeline/properties/index.tsx | 164 ---------- .../timeline/properties/properties_left.tsx | 188 ----------- .../properties/properties_right.test.tsx | 118 ------- .../timeline/properties/properties_right.tsx | 59 +--- .../components/timeline/properties/styles.tsx | 6 - .../timeline/properties/translations.ts | 30 -- .../components/timeline/timeline.tsx | 293 ++++++++++++------ .../components/timeline/translations.ts | 30 ++ 46 files changed, 594 insertions(+), 1196 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx 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 58fa6cdaeadd8..be80fa673cbc4 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 @@ -33,7 +33,6 @@ interface Props { onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: View) => void; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } const Details = styled.div` @@ -52,7 +51,6 @@ export const EventDetails = React.memo( onUpdateColumns, onViewSelected, timelineId, - toggleColumn, }) => { const handleTabClick = useCallback((e) => onViewSelected(e.id as View), [onViewSelected]); @@ -69,7 +67,6 @@ export const EventDetails = React.memo( eventId={id} onUpdateColumns={onUpdateColumns} timelineId={timelineId} - toggleColumn={toggleColumn} /> ), }, @@ -79,7 +76,7 @@ export const EventDetails = React.memo( content: , }, ], - [browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn] + [browserFields, columnHeaders, data, id, onUpdateColumns, timelineId] ); return ( 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 0acf461828bc3..f411e110a230d 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 @@ -32,7 +32,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -53,7 +52,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -79,7 +77,6 @@ describe('EventFieldsBrowser', () => { eventId={eventId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -101,7 +98,6 @@ describe('EventFieldsBrowser', () => { eventId={eventId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -124,7 +120,6 @@ describe('EventFieldsBrowser', () => { eventId={eventId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={toggleColumn} /> ); @@ -157,7 +152,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -184,7 +178,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -203,7 +196,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); @@ -224,7 +216,6 @@ describe('EventFieldsBrowser', () => { eventId={mockDetailItemDataId} onUpdateColumns={jest.fn()} timelineId="test" - toggleColumn={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 79250ae9bec52..2db695f946cd5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -6,8 +6,10 @@ import { sortBy } from 'lodash'; import { EuiInMemoryTable } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions } from '../../../timelines/store/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; @@ -23,12 +25,12 @@ interface Props { eventId: string; onUpdateColumns: OnUpdateColumns; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( - ({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId, toggleColumn }) => { + ({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId }) => { + const dispatch = useDispatch(); const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); const items = useMemo( () => @@ -39,6 +41,32 @@ export const EventFieldsBrowser = React.memo( })), [data, fieldsByName] ); + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columnHeaders.findIndex((c) => c.id === column.id) !== -1; + + if (!exists) { + dispatch( + timelineActions.upsertColumn({ + column, + id: timelineId, + index: 1, + }) + ); + } + + if (exists) { + dispatch( + timelineActions.removeColumn({ + columnId: column.id, + id: timelineId, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + const columns = useMemo( () => getColumns({ 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 3bd1f2f597a67..517660201512d 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 @@ -20,11 +20,10 @@ interface Props { id: string; onUpdateColumns: OnUpdateColumns; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } export const StatefulEventDetails = React.memo( - ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId }) => { // TODO: Move to the store const [view, setView] = useState('table-view'); @@ -37,7 +36,6 @@ export const StatefulEventDetails = React.memo( onUpdateColumns={onUpdateColumns} onViewSelected={setView} timelineId={timelineId} - toggleColumn={toggleColumn} view={view} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index aac1f4f2687eb..18f3e32bef479 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -81,7 +81,6 @@ const eventsViewerDefaultProps = { sortDirection: 'none' as SortDirection, }, scopeId: SourcererScopeName.timeline, - toggleColumn: jest.fn(), utilityBar, }; 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 92820c9c52b25..49dbe44232bc0 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 @@ -132,7 +132,6 @@ interface Props { onRuleChange?: () => void; start: string; sort: Sort; - toggleColumn: (column: ColumnHeaderOptions) => void; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; @@ -160,7 +159,6 @@ const EventsViewerComponent: React.FC = ({ onRuleChange, start, sort, - toggleColumn, utilityBar, graphEventId, }) => { @@ -342,7 +340,6 @@ const EventsViewerComponent: React.FC = ({ onRuleChange={onRuleChange} refetch={refetch} sort={sort} - toggleColumn={toggleColumn} />
= ({ docValueFields={docValueFields} event={expanded} timelineId={id} - toggleColumn={toggleColumn} /> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index a4f2b0536abf5..5a44e316e7f72 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -66,13 +66,11 @@ const StatefulEventsViewerComponent: React.FC = ({ pageFilters, query, onRuleChange, - removeColumn, start, scopeId, showCheckboxes, sort, updateItemsPerPage, - upsertColumn, utilityBar, // If truthy, the graph viewer (Resolver) is showing graphEventId, @@ -109,28 +107,6 @@ const StatefulEventsViewerComponent: React.FC = ({ [id, updateItemsPerPage] ); - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex((c) => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, upsertColumn, removeColumn] - ); - const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( @@ -158,7 +134,6 @@ const StatefulEventsViewerComponent: React.FC = ({ onRuleChange={onRuleChange} start={start} sort={sort} - toggleColumn={toggleColumn} utilityBar={utilityBar} graphEventId={graphEventId} /> @@ -214,8 +189,6 @@ const mapDispatchToProps = { createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, updateItemsPerPage: timelineActions.updateItemsPerPage, - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, }; const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx index 9340ee8cf0c7f..e3087149e1e93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx @@ -54,7 +54,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} />
@@ -92,7 +91,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -122,7 +120,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -148,7 +145,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -174,7 +170,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -200,7 +195,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -232,7 +226,6 @@ describe('FieldsBrowser', () => { onSearchInputChange={onSearchInputChange} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 3c9101878be8d..5481f6e4d348f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -86,10 +86,6 @@ type Props = Pick< * Invoked when the user types in the search input */ onSearchInputChange: (newSearchInput: string) => void; - /** - * Invoked to add or remove a column from the timeline - */ - toggleColumn: (column: ColumnHeaderOptions) => void; }; /** @@ -110,7 +106,6 @@ const FieldsBrowserComponent: React.FC = ({ searchInput, selectedCategoryId, timelineId, - toggleColumn, width, }) => { /** Focuses the input that filters the field browser */ @@ -219,7 +214,6 @@ const FieldsBrowserComponent: React.FC = ({ searchInput={searchInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} - toggleColumn={toggleColumn} width={FIELDS_PANE_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx index c2ddba6bd88c3..29debc52adb95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx @@ -33,7 +33,6 @@ describe('FieldsPane', () => { searchInput="" selectedCategoryId={selectedCategory} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -58,7 +57,6 @@ describe('FieldsPane', () => { searchInput="" selectedCategoryId={selectedCategory} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -83,7 +81,6 @@ describe('FieldsPane', () => { searchInput={searchInput} selectedCategoryId="" timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -108,7 +105,6 @@ describe('FieldsPane', () => { searchInput={searchInput} selectedCategoryId="" timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx index 73ea739216857..e9ee779d8fee6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx @@ -5,11 +5,13 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { timelineActions } from '../../../timelines/store/timeline'; import { Category } from './category'; import { FieldBrowserProps } from './types'; @@ -53,10 +55,6 @@ type Props = Pick void; }; export const FieldsPane = React.memo( ({ @@ -67,40 +65,68 @@ export const FieldsPane = React.memo( searchInput, selectedCategoryId, timelineId, - toggleColumn, width, - }) => ( - <> - {Object.keys(filteredBrowserFields).length > 0 ? ( - - ) : ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- )} - - ) + }) => { + const dispatch = useDispatch(); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columnHeaders.findIndex((c) => c.id === column.id) !== -1; + + if (!exists) { + dispatch( + timelineActions.upsertColumn({ + column, + id: timelineId, + index: 1, + }) + ); + } + + if (exists) { + dispatch( + timelineActions.removeColumn({ + columnId: column.id, + id: timelineId, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + return ( + <> + {Object.keys(filteredBrowserFields).length > 0 ? ( + + ) : ( + + + +

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

+
+
+
+ )} + + ); + } ); FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 3bfeabc614ea9..6e122b537ad66 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -29,7 +29,6 @@ describe('StatefulFieldsBrowser', () => { height={FIELD_BROWSER_HEIGHT} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -48,7 +47,6 @@ describe('StatefulFieldsBrowser', () => { height={FIELD_BROWSER_HEIGHT} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -66,7 +64,6 @@ describe('StatefulFieldsBrowser', () => { height={FIELD_BROWSER_HEIGHT} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -91,7 +88,6 @@ describe('StatefulFieldsBrowser', () => { height={FIELD_BROWSER_HEIGHT} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -117,7 +113,6 @@ describe('StatefulFieldsBrowser', () => { height={FIELD_BROWSER_HEIGHT} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -154,7 +149,6 @@ describe('StatefulFieldsBrowser', () => { isEventViewer={isEventViewer} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -175,7 +169,6 @@ describe('StatefulFieldsBrowser', () => { isEventViewer={isEventViewer} onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index f197d241cc422..0d6c0907e8862 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -39,7 +39,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ onFieldSelected, onUpdateColumns, timelineId, - toggleColumn, width, }) => { /** tracks the latest timeout id from `setTimeout`*/ @@ -173,7 +172,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ searchInput={filterInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} - toggleColumn={toggleColumn} width={width} /> )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts index 2b9889ec13e79..2bcbf7750cd9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts @@ -30,8 +30,6 @@ export interface FieldBrowserProps { onUpdateColumns: OnUpdateColumns; /** The timeline associated with this field browser */ timelineId: string; - /** Adds or removes a column to / from the timeline */ - toggleColumn: (column: ColumnHeaderOptions) => void; /** The width of the field browser */ width: number; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 0737db7a00788..5f6696b666911 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; - import { isEmpty, get } from 'lodash/fp'; +import styled from 'styled-components'; + import { TimelineType } from '../../../../../common/types/timeline'; import { History } from '../../../../common/lib/history'; -import { Note } from '../../../../common/lib/note'; -import { appSelectors, inputsModel, inputsSelectors, State } from '../../../../common/store'; -import { Properties } from '../../timeline/properties'; -import { appActions } from '../../../../common/store/app'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { TimelineProperties } from '../../timeline/properties/styles'; +import { PropertiesRight } from '../../timeline/properties/properties_right'; import { inputsActions } from '../../../../common/store/inputs'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; +import { Description, Name, StarIcon } from '../../timeline/properties/helpers'; + +import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; +import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants'; interface OwnProps { timelineId: string; @@ -28,52 +34,95 @@ interface OwnProps { type Props = OwnProps & PropsFromRedux; +export const PropertiesLeftStyle = styled(EuiFlexGroup)` + width: 100%; +`; + +PropertiesLeftStyle.displayName = 'PropertiesLeftStyle'; + +export const LockIconContainer = styled(EuiFlexItem)` + margin-right: 2px; +`; + +LockIconContainer.displayName = 'LockIconContainer'; + const StatefulFlyoutHeader = React.memo( ({ - associateNote, description, graphEventId, isDataInTimeline, isDatepickerLocked, isFavorite, - noteIds, - notesById, status, timelineId, timelineType, title, - toggleLock, updateDescription, updateIsFavorite, - updateNote, updateTitle, usersViewing, }) => { - const getNotesByIds = useCallback( - (noteIdsVar: string[]): Note[] => appSelectors.getNotes(notesById, noteIdsVar), - [notesById] - ); + const [showActions, setShowActions] = useState(false); + const [showTimelineModal, setShowTimelineModal] = useState(false); + + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onOpenTimelineModal = useCallback(() => { + onClosePopover(); + setShowTimelineModal(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); + return ( - + + + + + + + + + {ENABLE_NEW_TIMELINE && } + + + + + + 0} + status={status} + timelineId={timelineId} + timelineType={timelineType} + title={title} + usersViewing={usersViewing} + /> + + ); } ); @@ -82,11 +131,8 @@ StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader'; const emptyHistory: History[] = []; // stable reference -const emptyNotesId: string[] = []; // stable reference - const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getNotesByIds = appSelectors.notesByIdsSelector(); const getGlobalInput = inputsSelectors.globalSelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; @@ -98,7 +144,6 @@ const makeMapStateToProps = () => { isFavorite = false, kqlQuery, title = '', - noteIds = emptyNotesId, status, timelineType = TimelineType.default, } = timeline; @@ -113,8 +158,6 @@ const makeMapStateToProps = () => { !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), isFavorite, isDatepickerLocked: globalInput.linkTo.includes('timeline'), - noteIds, - notesById: getNotesByIds(state), status, title, timelineType, @@ -124,7 +167,6 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ - associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), updateDescription: ({ id, description, @@ -136,7 +178,6 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), - updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), updateTitle: ({ id, title, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx index a4d9f0e8293df..d20f5aef0c2d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx @@ -17,6 +17,7 @@ const FlyoutHeaderContainer = styled.div` flex-direction: row; justify-content: space-between; width: 100%; + padding: 16px; `; // manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx index 01dfd72a22db1..2dbd341d7e102 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx @@ -14,7 +14,6 @@ describe('AddNote', () => { const note = 'The contents of a new note'; const props = { associateNote: jest.fn(), - getNewNoteId: jest.fn(), newNote: note, onCancelAddNote: jest.fn(), updateNewNote: jest.fn(), @@ -93,19 +92,19 @@ describe('AddNote', () => { expect(associateNote).toBeCalled(); }); - test('it invokes getNewNoteId when the Add Note button is clicked', () => { - const getNewNoteId = jest.fn(); - const testProps = { - ...props, - getNewNoteId, - }; + // test('it invokes getNewNoteId when the Add Note button is clicked', () => { + // const getNewNoteId = jest.fn(); + // const testProps = { + // ...props, + // getNewNoteId, + // }; - const wrapper = mount(); + // const wrapper = mount(); - wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); + // wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); - expect(getNewNoteId).toBeCalled(); - }); + // expect(getNewNoteId).toBeCalled(); + // }); test('it invokes updateNewNote when the Add Note button is clicked', () => { const updateNewNote = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx index 6ba62a115917f..259cc2d0feb61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx @@ -7,14 +7,11 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; -import { - AssociateNote, - GetNewNoteId, - updateAndAssociateNode, - UpdateInternalNewNote, - UpdateNote, -} from '../helpers'; +import { appActions } from '../../../../common/store/app'; +import { Note } from '../../../../common/lib/note'; +import { AssociateNote, updateAndAssociateNode, UpdateInternalNewNote } from '../helpers'; import * as i18n from '../translations'; import { NewNote } from './new_note'; @@ -43,23 +40,27 @@ CancelButton.displayName = 'CancelButton'; /** Displays an input for entering a new note, with an adjacent "Add" button */ export const AddNote = React.memo<{ associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; newNote: string; onCancelAddNote?: () => void; updateNewNote: UpdateInternalNewNote; - updateNote: UpdateNote; -}>(({ associateNote, getNewNoteId, newNote, onCancelAddNote, updateNewNote, updateNote }) => { +}>(({ associateNote, newNote, onCancelAddNote, updateNewNote }) => { + const dispatch = useDispatch(); + + const updateNote = useCallback((note: Note) => dispatch(appActions.updateNote({ note })), [ + dispatch, + ]); + const handleClick = useCallback( () => updateAndAssociateNode({ associateNote, - getNewNoteId, newNote, updateNewNote, updateNote, }), - [associateNote, getNewNoteId, newNote, updateNewNote, updateNote] + [associateNote, newNote, updateNewNote, updateNote] ); + return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx index 938bc0d222002..a4622f58d34b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx @@ -8,6 +8,7 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import moment from 'moment'; import React from 'react'; import styled from 'styled-components'; +import uuid from 'uuid'; import { Note } from '../../../common/lib/note'; @@ -24,8 +25,6 @@ export type GetNewNoteId = () => string; export type UpdateInternalNewNote = (newNote: string) => void; /** Closes the notes popover */ export type OnClosePopover = () => void; -/** Performs IO to associate a note with an event */ -export type AddNoteToEvent = ({ eventId, noteId }: { eventId: string; noteId: string }) => void; /** * Defines the behavior of the search input that appears above the table of data @@ -75,15 +74,9 @@ export const NotesCount = React.memo<{ NotesCount.displayName = 'NotesCount'; /** Creates a new instance of a `note` */ -export const createNote = ({ - newNote, - getNewNoteId, -}: { - newNote: string; - getNewNoteId: GetNewNoteId; -}): Note => ({ +export const createNote = ({ newNote }: { newNote: string }): Note => ({ created: moment.utc().toDate(), - id: getNewNoteId(), + id: uuid.v4(), lastEdit: null, note: newNote.trim(), saveObjectId: null, @@ -93,7 +86,6 @@ export const createNote = ({ interface UpdateAndAssociateNodeParams { associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; newNote: string; updateNewNote: UpdateInternalNewNote; updateNote: UpdateNote; @@ -101,12 +93,11 @@ interface UpdateAndAssociateNodeParams { export const updateAndAssociateNode = ({ associateNote, - getNewNoteId, newNote, updateNewNote, updateNote, }: UpdateAndAssociateNodeParams) => { - const note = createNote({ newNote, getNewNoteId }); + const note = createNote({ newNote }); updateNote(note); // perform IO to store the newly-created note associateNote(note.id); // associate the note with the (opaque) thing updateNewNote(''); // clear the input diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx index 7d083735e6c71..e55c42b459901 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx @@ -11,23 +11,24 @@ import { EuiModalHeader, EuiSpacer, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { Note } from '../../../common/lib/note'; import { AddNote } from './add_note'; import { columns } from './columns'; -import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; +import { AssociateNote, NotesCount, search } from './helpers'; import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; +import { timelineActions } from '../../store/timeline'; +import { appSelectors } from '../../../common/store/app'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; interface Props { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; noteIds: string[]; status: TimelineStatusLiteral; - updateNote: UpdateNote; } const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( @@ -41,39 +42,78 @@ const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( InMemoryTable.displayName = 'InMemoryTable'; /** A view for entering and reviewing notes */ -export const Notes = React.memo( - ({ associateNote, getNotesByIds, getNewNoteId, noteIds, status, updateNote }) => { +export const Notes = React.memo(({ associateNote, noteIds, status }) => { + const getNotesByIds = appSelectors.notesByIdsSelector(); + const [newNote, setNewNote] = useState(''); + const isImmutable = status === TimelineStatus.immutable; + + const notesById = useDeepEqualSelector(getNotesByIds); + + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + + return ( + <> + + + + + + {!isImmutable && ( + + )} + + + + + ); +}); + +Notes.displayName = 'Notes'; + +interface NotesTabContentPros { + noteIds: string[]; + timelineId: string; + timelineStatus: TimelineStatusLiteral; +} + +/** A view for entering and reviewing notes */ +export const NotesTabContent = React.memo( + ({ noteIds, timelineStatus, timelineId }) => { + const dispatch = useDispatch(); + const getNotesByIds = appSelectors.notesByIdsSelector(); const [newNote, setNewNote] = useState(''); - const isImmutable = status === TimelineStatus.immutable; + const isImmutable = timelineStatus === TimelineStatus.immutable; + const notesById = useDeepEqualSelector(getNotesByIds); + + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + + const associateNote = useCallback( + (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), + [dispatch, timelineId] + ); return ( <> - - - - - - {!isImmutable && ( - - )} - - - + + {!isImmutable && ( + + )} + ); } ); -Notes.displayName = 'Notes'; +NotesTabContent.displayName = 'NotesTabContent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx index 54a074bb2adb7..622e089fc67d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx @@ -43,7 +43,6 @@ describe('NoteCards', () => { const props = { associateNote: jest.fn(), getNotesByIds, - getNewNoteId: jest.fn(), noteIds, showAddNote: true, status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 62d169b1169dd..2761e2b2d7d48 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -5,14 +5,14 @@ */ import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { Note } from '../../../../common/lib/note'; +import { appSelectors } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { AddNote } from '../add_note'; -import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers'; +import { AssociateNote } from '../helpers'; import { NoteCard } from '../note_card'; -import { TimelineStatusLiteral } from '../../../../../common/types/timeline'; const AddNoteContainer = styled.div``; AddNoteContainer.displayName = 'AddNoteContainer'; @@ -46,27 +46,17 @@ NotesContainer.displayName = 'NotesContainer'; interface Props { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; noteIds: string[]; showAddNote: boolean; - status: TimelineStatusLiteral; toggleShowAddNote: () => void; - updateNote: UpdateNote; } /** A view for entering and reviewing notes */ export const NoteCards = React.memo( - ({ - associateNote, - getNotesByIds, - getNewNoteId, - noteIds, - showAddNote, - status, - toggleShowAddNote, - updateNote, - }) => { + ({ associateNote, noteIds, showAddNote, toggleShowAddNote }) => { + const getNotesByIds = appSelectors.notesByIdsSelector(); + const notesById = useDeepEqualSelector(getNotesByIds); + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); const [newNote, setNewNote] = useState(''); const associateNoteAndToggleShow = useCallback( @@ -81,7 +71,7 @@ export const NoteCards = React.memo( {noteIds.length ? ( - {getNotesByIds(noteIds).map((note) => ( + {items.map((note) => ( @@ -93,11 +83,9 @@ export const NoteCards = React.memo( ) : null} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 4c3be81a4992a..101428e4eb762 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -416,8 +416,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli } if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { - const getNewNoteId = (): string => uuid.v4(); - const newNote = createNote({ newNote: ruleNote, getNewNoteId }); + const newNote = createNote({ newNote: ruleNote }); dispatch(dispatchUpdateNote({ note: newNote })); dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx index a82821675d956..af8045bf624c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -7,39 +7,33 @@ import React from 'react'; import { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import { AssociateNote } from '../../../notes/helpers'; import * as i18n from '../translations'; import { NotesButton } from '../../properties/helpers'; -import { Note } from '../../../../../common/lib/note'; import { ActionIconItem } from './action_icon_item'; interface AddEventNoteActionProps { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; noteIds: string[]; showNotes: boolean; status: TimelineStatus; timelineType: TimelineType; toggleShowNotes: () => void; - updateNote: UpdateNote; } const AddEventNoteActionComponent: React.FC = ({ associateNote, - getNotesByIds, noteIds, showNotes, status, timelineType, toggleShowNotes, - updateNote, }) => ( = ({ toolTip={ timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP } - updateNote={updateNote} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index 6685ce7d7a018..e18ab72194bf1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -44,7 +44,6 @@ describe('ColumnHeaders', () => { showSelectAllCheckbox={false} sort={sort} timelineId={'test'} - toggleColumn={jest.fn()} /> ); @@ -68,7 +67,6 @@ describe('ColumnHeaders', () => { showSelectAllCheckbox={false} sort={sort} timelineId={'test'} - toggleColumn={jest.fn()} /> ); @@ -93,7 +91,6 @@ describe('ColumnHeaders', () => { showSelectAllCheckbox={false} sort={sort} timelineId={'test'} - toggleColumn={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index f4d4cf29ba38b..afad5db35bce0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -61,7 +61,6 @@ interface Props { showSelectAllCheckbox: boolean; sort: Sort; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } interface DraggableContainerProps { @@ -112,7 +111,6 @@ export const ColumnHeadersComponent = ({ showSelectAllCheckbox, sort, timelineId, - toggleColumn, }: Props) => { const [draggingIndex, setDraggingIndex] = useState(null); const { @@ -245,7 +243,6 @@ export const ColumnHeadersComponent = ({ height={FIELD_BROWSER_HEIGHT} onUpdateColumns={onUpdateColumns} timelineId={timelineId} - toggleColumn={toggleColumn} width={FIELD_BROWSER_WIDTH} /> @@ -313,7 +310,6 @@ export const ColumnHeaders = React.memo( prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && prevProps.sort === nextProps.sort && prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.browserFields, nextProps.browserFields) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 0d37f25d66e3f..32e2ae2141899 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -10,7 +10,6 @@ import { getOr } from 'lodash/fp'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { OnColumnResized } from '../../events'; import { EventsTd, EventsTdContent, EventsTdGroupData } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getColumnRenderer } from '../renderers/get_column_renderer'; @@ -21,7 +20,6 @@ interface Props { columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; - onColumnResized: OnColumnResized; timelineId: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index ae552ade665cb..49b89b4458d1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -41,7 +41,6 @@ describe('EventColumnView', () => { }, eventIdToNoteIds: {}, expanded: false, - getNotesByIds: jest.fn(), loading: false, loadingEventIds: [], onColumnResized: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 15d7d750257ac..a06c3729166d0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -5,14 +5,13 @@ */ import React, { useCallback, useMemo } from 'react'; -import uuid from 'uuid'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import { AssociateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; @@ -43,11 +42,9 @@ interface Props { ecsData: Ecs; eventIdToNoteIds: Readonly>; expanded: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; isEventPinned: boolean; isEventViewer?: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; @@ -59,11 +56,8 @@ interface Props { showNotes: boolean; timelineId: string; toggleShowNotes: () => void; - updateNote: UpdateNote; } -export const getNewNoteId = (): string => uuid.v4(); - const emptyNotes: string[] = []; export const EventColumnView = React.memo( @@ -77,11 +71,9 @@ export const EventColumnView = React.memo( ecsData, eventIdToNoteIds, expanded, - getNotesByIds, isEventPinned = false, isEventViewer = false, loadingEventIds, - onColumnResized, onEventToggled, onPinEvent, onRowSelected, @@ -93,7 +85,6 @@ export const EventColumnView = React.memo( showNotes, timelineId, toggleShowNotes, - updateNote, }) => { const { timelineType, status } = useShallowEqualSelector( (state) => state.timeline.timelineById[timelineId] @@ -134,11 +125,9 @@ export const EventColumnView = React.memo( , @@ -166,7 +155,6 @@ export const EventColumnView = React.memo( ecsData, eventIdToNoteIds, eventType, - getNotesByIds, handlePinClicked, id, isEventPinned, @@ -178,7 +166,6 @@ export const EventColumnView = React.memo( timelineId, timelineType, toggleShowNotes, - updateNote, ] ); @@ -203,7 +190,6 @@ export const EventColumnView = React.memo( columnRenderers={columnRenderers} data={data} ecsData={ecsData} - onColumnResized={onColumnResized} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index a1ef53bbafb6f..7983c9c57dd5a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -13,9 +13,7 @@ import { TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { Note } from '../../../../../common/lib/note'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; @@ -24,49 +22,37 @@ import { eventIsPinned } from '../helpers'; interface Props { actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineItem[]; expanded: { eventId?: string; indexName?: string }; eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; id: string; isEventViewer?: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; onEventToggled: (event: TimelineItem) => void; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; } const EventsComponent: React.FC = ({ actionsColumnWidth, - addNoteToEvent, browserFields, columnHeaders, columnRenderers, data, eventIdToNoteIds, expanded, - getNotesByIds, id, isEventViewer = false, loadingEventIds, - onColumnResized, - onPinEvent, onRowSelected, - onUnPinEvent, pinnedEventIds, refetch, onEventToggled, @@ -74,28 +60,22 @@ const EventsComponent: React.FC = ({ rowRenderers, selectedEventIds, showCheckboxes, - updateNote, }) => ( {data.map((event) => ( onEventToggled(event)} @@ -103,7 +83,6 @@ const EventsComponent: React.FC = ({ selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} timelineId={id} - updateNote={updateNote} /> ))} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 0d2d43b2d7399..0b869d3a39e0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -5,18 +5,15 @@ */ import React, { useRef, useState, useCallback } from 'react'; -import uuid from 'uuid'; +import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../../../common/containers/source'; -import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; @@ -27,24 +24,20 @@ import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; import { inputsModel } from '../../../../../common/store'; +import { timelineActions } from '../../../../store/timeline'; interface Props { actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; isEventViewer?: boolean; isExpanded: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; onEventToggled: () => void; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; isEventPinned: boolean; refetch: inputsModel.Refetch; onRuleChange?: () => void; @@ -52,11 +45,8 @@ interface Props { selectedEventIds: Readonly>; showCheckboxes: boolean; timelineId: string; - updateNote: UpdateNote; } -export const getNewNoteId = (): string => uuid.v4(); - const emptyNotes: string[] = []; const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { @@ -68,34 +58,26 @@ EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWra const StatefulEventComponent: React.FC = ({ actionsColumnWidth, - addNoteToEvent, browserFields, columnHeaders, columnRenderers, event, eventIdToNoteIds, - getNotesByIds, isEventViewer = false, isEventPinned = false, isExpanded = false, loadingEventIds, - onColumnResized, onEventToggled, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, timelineId, - updateNote, }) => { + const dispatch = useDispatch(); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const { status: timelineStatus } = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId] - ); const divElement = useRef(null); const onToggleShowNotes = useCallback(() => { @@ -103,14 +85,24 @@ const StatefulEventComponent: React.FC = ({ setShowNotes((prevShowNotes) => ({ ...prevShowNotes, [eventId]: !prevShowNotes[eventId] })); }, [event]); + const onPinEvent: OnPinEvent = useCallback( + (eventId) => dispatch(timelineActions.pinEvent!({ id: timelineId, eventId })), + [dispatch, timelineId] + ); + + const onUnPinEvent: OnPinEvent = useCallback( + (eventId) => dispatch(timelineActions.unPinEvent!({ id: timelineId, eventId })), + [dispatch, timelineId] + ); + const associateNote = useCallback( (noteId: string) => { - addNoteToEvent({ eventId: event._id, noteId }); + dispatch(timelineActions.addNoteToEvent({ eventId: event._id, id: timelineId, noteId })); if (!isEventPinned) { onPinEvent(event._id); // pin the event, because it has notes } }, - [addNoteToEvent, event, isEventPinned, onPinEvent] + [dispatch, event, isEventPinned, onPinEvent, timelineId] ); return ( @@ -133,11 +125,9 @@ const StatefulEventComponent: React.FC = ({ ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} expanded={isExpanded} - getNotesByIds={getNotesByIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} - onColumnResized={onColumnResized} onEventToggled={onEventToggled} onPinEvent={onPinEvent} onRowSelected={onRowSelected} @@ -149,7 +139,6 @@ const StatefulEventComponent: React.FC = ({ showNotes={!!showNotes[event._id]} timelineId={timelineId} toggleShowNotes={onToggleShowNotes} - updateNote={updateNote} /> @@ -160,13 +149,9 @@ const StatefulEventComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 2186cac42597b..698b127de9fa6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -19,7 +19,6 @@ import { waitFor } from '@testing-library/react'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; -const mockGetNotesByIds = (eventId: string[]) => []; const mockSort: Sort = { columnId: '@timestamp', sortDirection: Direction.desc, @@ -51,7 +50,6 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ describe('Body', () => { const mount = useMountAppended(); const props: BodyProps = { - addNoteToEvent: jest.fn(), browserFields: mockBrowserFields, columnHeaders: defaultHeaders, columnRenderers, @@ -60,16 +58,13 @@ describe('Body', () => { eventIdToNoteIds: {}, expanded: {}, isSelectAllChecked: false, - getNotesByIds: mockGetNotesByIds, loadingEventIds: [], onColumnRemoved: jest.fn(), onColumnResized: jest.fn(), onColumnSorted: jest.fn(), onEventToggled: jest.fn(), - onPinEvent: jest.fn(), onRowSelected: jest.fn(), onSelectAll: jest.fn(), - onUnPinEvent: jest.fn(), onUpdateColumns: jest.fn(), pinnedEventIds: {}, refetch: jest.fn(), @@ -79,8 +74,6 @@ describe('Body', () => { sort: mockSort, showCheckboxes: false, timelineId: 'timeline-test', - toggleColumn: jest.fn(), - updateNote: jest.fn(), }; describe('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 69fd107e3c204..fd3d7bb1081a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -9,17 +9,13 @@ import React, { useMemo } from 'react'; import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { Note } from '../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, OnColumnResized, OnColumnSorted, - OnPinEvent, OnRowSelected, OnSelectAll, - OnUnPinEvent, OnUpdateColumns, } from '../events'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; @@ -34,14 +30,12 @@ import { TimelineEventsType, TimelineId } from '../../../../../common/types/time import { ActiveTimelineExpandedEvent } from '../../../containers/active_timeline_context'; export interface BodyProps { - addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineItem[]; docValueFields: DocValueFields[]; expanded: ActiveTimelineExpandedEvent; - getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -54,9 +48,7 @@ export interface BodyProps { onEventToggled: (event: TimelineItem) => void; onRowSelected: OnRowSelected; onSelectAll: OnSelectAll; - onPinEvent: OnPinEvent; onUpdateColumns: OnUpdateColumns; - onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; onRuleChange?: () => void; @@ -66,8 +58,6 @@ export interface BodyProps { showCheckboxes: boolean; sort: Sort; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; } export const hasAdditionalActions = (id: TimelineId): boolean => @@ -80,14 +70,12 @@ const EXTRA_WIDTH = 4; // px /** Renders the timeline body */ export const Body = React.memo( ({ - addNoteToEvent, browserFields, columnHeaders, columnRenderers, data, eventIdToNoteIds, expanded, - getNotesByIds, graphEventId, isEventViewer = false, isSelectAllChecked, @@ -98,9 +86,7 @@ export const Body = React.memo( onEventToggled, onRowSelected, onSelectAll, - onPinEvent, onUpdateColumns, - onUnPinEvent, pinnedEventIds, rowRenderers, refetch, @@ -109,9 +95,7 @@ export const Body = React.memo( show, showCheckboxes, sort, - toggleColumn, timelineId, - updateNote, }) => { const actionsColumnWidth = useMemo( () => @@ -154,26 +138,20 @@ export const Body = React.memo( showSelectAllCheckbox={showCheckboxes} sort={sort} timelineId={timelineId} - toggleColumn={toggleColumn} /> ( onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} - toggleColumn={toggleColumn} - updateNote={updateNote} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 3e49e1f87f791..50b0424b31f71 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -12,22 +12,17 @@ import deepEqual from 'fast-deep-equal'; import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../../common/search_strategy/timeline'; -import { Note } from '../../../../common/lib/note'; -import { appSelectors, inputsModel, State } from '../../../../common/store'; -import { appActions } from '../../../../common/store/actions'; +import { inputsModel, State } from '../../../../common/store'; import { useManageTimeline } from '../../manage_timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, OnColumnResized, OnColumnSorted, - OnPinEvent, OnRowSelected, OnSelectAll, - OnUnPinEvent, OnUpdateColumns, } from '../events'; import { getColumnHeaders } from './column_headers/helpers'; @@ -47,7 +42,6 @@ interface OwnProps { isEventViewer?: boolean; onEventToggled: (event: TimelineItem) => void; sort: Sort; - toggleColumn: (column: ColumnHeaderOptions) => void; refetch: inputsModel.Refetch; onRuleChange?: () => void; } @@ -58,7 +52,6 @@ export const emptyColumnHeaders: ColumnHeaderOptions[] = []; const StatefulBodyComponent = React.memo( ({ - addNoteToEvent, applyDeltaToColumnWidth, browserFields, columnHeaders, @@ -71,8 +64,6 @@ const StatefulBodyComponent = React.memo( isEventViewer = false, isSelectAllChecked, loadingEventIds, - notesById, - pinEvent, pinnedEventIds, removeColumn, selectedEventIds, @@ -85,10 +76,7 @@ const StatefulBodyComponent = React.memo( graphEventId, refetch, sort, - toggleColumn, - unPinEvent, updateColumns, - updateNote, updateSort, }) => { const { getManageTimelineById } = useManageTimeline(); @@ -97,17 +85,6 @@ const StatefulBodyComponent = React.memo( id, ]); - const getNotesByIds = useCallback( - (noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds), - [notesById] - ); - - const onAddNoteToEvent: AddNoteToEvent = useCallback( - ({ eventId, noteId }: { eventId: string; noteId: string }) => - addNoteToEvent!({ id, eventId, noteId }), - [id, addNoteToEvent] - ); - const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { setSelected!({ @@ -155,20 +132,6 @@ const StatefulBodyComponent = React.memo( [applyDeltaToColumnWidth, id] ); - const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [ - id, - pinEvent, - ]); - - const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [ - id, - unPinEvent, - ]); - - const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [ - updateNote, - ]); - const onUpdateColumns: OnUpdateColumns = useCallback( (columns) => updateColumns!({ id, columns }), [id, updateColumns] @@ -195,7 +158,6 @@ const StatefulBodyComponent = React.memo( return ( ( docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} expanded={expanded} - getNotesByIds={getNotesByIds} graphEventId={graphEventId} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} @@ -214,8 +175,6 @@ const StatefulBodyComponent = React.memo( onEventToggled={onEventToggled} onRowSelected={onRowSelected} onSelectAll={onSelectAll} - onPinEvent={onPinEvent} - onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} refetch={refetch} @@ -226,8 +185,6 @@ const StatefulBodyComponent = React.memo( showCheckboxes={showCheckboxes} sort={sort} timelineId={id} - toggleColumn={toggleColumn} - updateNote={onUpdateNote} /> ); }, @@ -240,7 +197,6 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && prevProps.graphEventId === nextProps.graphEventId && - deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && @@ -262,7 +218,6 @@ const makeMapStateToProps = () => { ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getNotesByIds = appSelectors.notesByIdsSelector(); const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const { @@ -285,7 +240,6 @@ const makeMapStateToProps = () => { graphEventId, isSelectAllChecked, loadingEventIds, - notesById: getNotesByIds(state), id, pinnedEventIds, selectedEventIds, @@ -297,16 +251,12 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = { - addNoteToEvent: timelineActions.addNoteToEvent, applyDeltaToColumnWidth: timelineActions.applyDeltaToColumnWidth, clearSelected: timelineActions.clearSelected, - pinEvent: timelineActions.pinEvent, removeColumn: timelineActions.removeColumn, removeProvider: timelineActions.removeProvider, setSelected: timelineActions.setSelected, - unPinEvent: timelineActions.unPinEvent, updateColumns: timelineActions.updateColumns, - updateNote: appActions.updateNote, updateSort: timelineActions.updateSort, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 4f47ced0be6f8..4ac12ff2803b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -11,7 +11,6 @@ import { useDispatch } from 'react-redux'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import { StatefulEventDetails } from '../../../../common/components/event_details/stateful_event_details'; import { LazyAccordion } from '../../lazy_accordion'; import { useTimelineEventsDetails } from '../../../containers/details'; @@ -34,11 +33,10 @@ interface Props { docValueFields: DocValueFields[]; event: ActiveTimelineExpandedEvent; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } export const ExpandableEvent = React.memo( - ({ browserFields, docValueFields, event, timelineId, toggleColumn }) => { + ({ browserFields, docValueFields, event, timelineId }) => { const dispatch = useDispatch(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); @@ -69,18 +67,9 @@ export const ExpandableEvent = React.memo( id={event.eventId!} onUpdateColumns={onUpdateColumns} timelineId={timelineId} - toggleColumn={toggleColumn} /> ), - [ - browserFields, - columnHeaders, - detailsData, - event.eventId, - onUpdateColumns, - timelineId, - toggleColumn, - ] + [browserFields, columnHeaders, detailsData, event.eventId, onUpdateColumns, timelineId] ); if (!event.eventId) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 35d31e034e7f3..01dbd91efc60f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -11,13 +11,14 @@ import deepEqual from 'fast-deep-equal'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; -import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; import { OnChangeItemsPerPage } from './events'; import { Timeline } from './timeline'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { inputsActions } from '../../../common/store/inputs'; export interface OwnProps { id: string; @@ -41,6 +42,7 @@ const StatefulTimelineComponent = React.memo( filters, graphEventId, id, + isDatePickerLocked, isLive, isSaving, isTimelineExists, @@ -49,7 +51,7 @@ const StatefulTimelineComponent = React.memo( kqlMode, kqlQueryExpression, onClose, - removeColumn, + noteIds, show, showCallOutUnauthorizedMsg, sort, @@ -57,8 +59,8 @@ const StatefulTimelineComponent = React.memo( status, timelineType, timerangeKind, + toggleLock, updateItemsPerPage, - upsertColumn, usersViewing, }) => { const { @@ -74,28 +76,6 @@ const StatefulTimelineComponent = React.memo( [id, updateItemsPerPage] ); - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex((c) => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, removeColumn, upsertColumn] - ); - useEffect(() => { if (createTimeline != null && !isTimelineExists) { createTimeline({ id, columns: defaultHeaders, indexNames: selectedPatterns, show: false }); @@ -115,6 +95,7 @@ const StatefulTimelineComponent = React.memo( id={id} indexPattern={indexPattern} indexNames={selectedPatterns} + isDatePickerLocked={isDatePickerLocked} isLive={isLive} isSaving={isSaving} itemsPerPage={itemsPerPage!} @@ -122,6 +103,7 @@ const StatefulTimelineComponent = React.memo( kqlMode={kqlMode} kqlQueryExpression={kqlQueryExpression} loadingSourcerer={loading} + noteIds={noteIds} onChangeItemsPerPage={onChangeItemsPerPage} onClose={onClose} show={show!} @@ -129,36 +111,36 @@ const StatefulTimelineComponent = React.memo( sort={sort!} start={start} status={status} - toggleColumn={toggleColumn} timelineType={timelineType} timerangeKind={timerangeKind} + toggleLock={toggleLock} usersViewing={usersViewing} /> ); }, - (prevProps, nextProps) => { - return ( - isTimerangeSame(prevProps, nextProps) && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.id === nextProps.id && - prevProps.isLive === nextProps.isLive && - prevProps.isSaving === nextProps.isSaving && - prevProps.isTimelineExists === nextProps.isTimelineExists && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.kqlMode === nextProps.kqlMode && - prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && - prevProps.show === nextProps.show && - prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.timelineType === nextProps.timelineType && - prevProps.status === nextProps.status && - deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.usersViewing, nextProps.usersViewing) - ); - } + // eslint-disable-next-line complexity + (prevProps, nextProps) => + isTimerangeSame(prevProps, nextProps) && + prevProps.graphEventId === nextProps.graphEventId && + prevProps.id === nextProps.id && + prevProps.isDatePickerLocked === nextProps.isDatePickerLocked && + prevProps.isLive === nextProps.isLive && + prevProps.isSaving === nextProps.isSaving && + prevProps.isTimelineExists === nextProps.isTimelineExists && + prevProps.itemsPerPage === nextProps.itemsPerPage && + prevProps.kqlMode === nextProps.kqlMode && + prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && + prevProps.timelineType === nextProps.timelineType && + prevProps.status === nextProps.status && + deepEqual(prevProps.noteIds, nextProps.noteIds) && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.usersViewing, nextProps.usersViewing) ); StatefulTimelineComponent.displayName = 'StatefulTimelineComponent'; @@ -168,9 +150,11 @@ const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getGlobalInput = inputsSelectors.globalSelector(); const mapStateToProps = (state: State, { id }: OwnProps) => { const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const input: inputsModel.InputsRange = getInputsTimeline(state); + const globalInput: inputsModel.InputsRange = getGlobalInput(state); const { columns, dataProviders, @@ -181,6 +165,7 @@ const makeMapStateToProps = () => { itemsPerPageOptions, isSaving, kqlMode, + noteIds, show, sort, status, @@ -202,6 +187,7 @@ const makeMapStateToProps = () => { filters: timelineFilter, graphEventId, id, + isDatePickerLocked: globalInput.linkTo.includes('timeline'), isLive: input.policy.kind === 'interval', isSaving, isTimelineExists: getTimeline(state, id) != null, @@ -209,6 +195,7 @@ const makeMapStateToProps = () => { itemsPerPageOptions, kqlMode, kqlQueryExpression, + noteIds, show, showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), sort, @@ -222,15 +209,12 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = { - addProvider: timelineActions.addProvider, createTimeline: timelineActions.createTimeline, - removeColumn: timelineActions.removeColumn, + toggleLock: inputsActions.toggleTimelineLinkTo, updateColumns: timelineActions.updateColumns, - updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, updateItemsPerPage: timelineActions.updateItemsPerPage, updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, updateSort: timelineActions.updateSort, - upsertColumn: timelineActions.upsertColumn, }; const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 25039dbc9529a..3780eed7e893e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -19,7 +19,6 @@ import { EuiTextArea, } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import uuid from 'uuid'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -35,21 +34,13 @@ import { SecurityPageName } from '../../../../app/types'; import { timelineSelectors } from '../../../../timelines/store/timeline'; import { getCreateCaseUrl } from '../../../../common/components/link_to'; import { useKibana } from '../../../../common/lib/kibana'; -import { Note } from '../../../../common/lib/note'; import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; import { Notes } from '../../notes'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { AssociateNote } from '../../notes/helpers'; import { NOTES_PANEL_WIDTH } from './notes_size'; -import { - ButtonContainer, - DescriptionContainer, - LabelText, - NameField, - NameWrapper, - StyledStar, -} from './styles'; +import { ButtonContainer, DescriptionContainer, LabelText, NameField, NameWrapper } from './styles'; import * as i18n from './translations'; import { setInsertTimeline, showTimeline, TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; @@ -107,21 +98,14 @@ export const StarIcon = React.memo<{ ]); return ( - // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener - // TODO: 2 error is: Elements with the 'button' interactive role must be focusable - // TODO: Investigate this error - // eslint-disable-next-line -
- {isFavorite ? ( - - - - ) : ( - - - - )} -
+ + {isFavorite ? i18n.NOT_A_FAVORITE : i18n.FAVORITE} + ); }); StarIcon.displayName = 'StarIcon'; @@ -412,7 +396,6 @@ NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { animate?: boolean; associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; noteIds: string[]; size: 's' | 'l'; status: TimelineStatusLiteral; @@ -420,12 +403,9 @@ interface NotesButtonProps { toggleShowNotes: () => void; text?: string; toolTip?: string; - updateNote: UpdateNote; timelineType: TimelineTypeLiteral; } -const getNewNoteId = (): string => uuid.v4(); - interface LargeNotesButtonProps { noteIds: string[]; text?: string; @@ -433,11 +413,7 @@ interface LargeNotesButtonProps { } const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( - toggleShowNotes()} - size="m" - > + @@ -468,7 +444,7 @@ const SmallNotesButton = React.memo(({ toggleShowNotes, t aria-label={i18n.NOTES} data-test-subj="timeline-notes-button-small" iconType="editorComment" - onClick={() => toggleShowNotes()} + onClick={toggleShowNotes} isDisabled={isTemplate} /> ); @@ -482,14 +458,12 @@ const NotesButtonComponent = React.memo( ({ animate = true, associateNote, - getNotesByIds, noteIds, showNotes, size, status, toggleShowNotes, text, - updateNote, timelineType, }) => ( @@ -506,14 +480,7 @@ const NotesButtonComponent = React.memo( maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes} > - + ) : null} @@ -527,7 +494,6 @@ export const NotesButton = React.memo( ({ animate = true, associateNote, - getNotesByIds, noteIds, showNotes, size, @@ -536,20 +502,17 @@ export const NotesButton = React.memo( toggleShowNotes, toolTip, text, - updateNote, }) => showNotes ? ( ) : ( @@ -557,14 +520,12 @@ export const NotesButton = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index cdedca23e85af..9a2e6fa7caf72 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -90,7 +90,6 @@ const defaultProps = { title: '', timelineType: TimelineType.default, description: '', - getNotesByIds: jest.fn(), noteIds: [], saveTimeline: jest.fn(), status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 9df2b585449a0..41bc2aa258807 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -3,167 +3,3 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import React, { useState, useCallback, useMemo } from 'react'; - -import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline'; -import { useThrottledResizeObserver } from '../../../../common/components/utils'; -import { Note } from '../../../../common/lib/note'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; - -import { AssociateNote, UpdateNote } from '../../notes/helpers'; - -import { TimelineProperties } from './styles'; -import { PropertiesRight } from './properties_right'; -import { PropertiesLeft } from './properties_left'; -import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; - -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; - -interface Props { - associateNote: AssociateNote; - description: string; - getNotesByIds: (noteIds: string[]) => Note[]; - graphEventId?: string; - isDataInTimeline: boolean; - isDatepickerLocked: boolean; - isFavorite: boolean; - noteIds: string[]; - timelineId: string; - timelineType: TimelineTypeLiteral; - status: TimelineStatusLiteral; - title: string; - toggleLock: ToggleLock; - updateDescription: UpdateDescription; - updateIsFavorite: UpdateIsFavorite; - updateNote: UpdateNote; - updateTitle: UpdateTitle; - usersViewing: string[]; -} - -const rightGutter = 60; // px -export const datePickerThreshold = 600; -export const showNotesThreshold = 810; -export const showDescriptionThreshold = 970; - -const starIconWidth = 30; -const nameWidth = 155; -const descriptionWidth = 165; -const noteWidth = 130; -const settingsWidth = 55; - -/** Displays the properties of a timeline, i.e. name, description, notes, etc */ -export const Properties = React.memo( - ({ - associateNote, - description, - getNotesByIds, - graphEventId, - isDataInTimeline, - isDatepickerLocked, - isFavorite, - noteIds, - status, - timelineId, - timelineType, - title, - toggleLock, - updateDescription, - updateIsFavorite, - updateNote, - updateTitle, - usersViewing, - }) => { - const { ref, width = 0 } = useThrottledResizeObserver(300); - const [showActions, setShowActions] = useState(false); - const [showNotes, setShowNotes] = useState(false); - const [showTimelineModal, setShowTimelineModal] = useState(false); - - const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); - const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); - const onClosePopover = useCallback(() => setShowActions(false), []); - const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); - const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); - const onOpenTimelineModal = useCallback(() => { - onClosePopover(); - setShowTimelineModal(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); - - const datePickerWidth = useMemo( - () => - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth, - [width] - ); - - return ( - - datePickerThreshold ? datePickerThreshold : datePickerWidth - } - description={description} - getNotesByIds={getNotesByIds} - isDatepickerLocked={isDatepickerLocked} - isFavorite={isFavorite} - noteIds={noteIds} - onToggleShowNotes={onToggleShowNotes} - status={status} - showDescription={width >= showDescriptionThreshold} - showNotes={showNotes} - showNotesFromWidth={width >= showNotesThreshold} - timelineId={timelineId} - timelineType={timelineType} - title={title} - toggleLock={onToggleLock} - updateDescription={updateDescription} - updateIsFavorite={updateIsFavorite} - updateNote={updateNote} - updateTitle={updateTitle} - /> - 0} - status={status} - timelineId={timelineId} - timelineType={timelineType} - title={title} - updateDescription={updateDescription} - updateNote={updateNote} - usersViewing={usersViewing} - /> - - - ); - } -); - -Properties.displayName = 'Properties'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx deleted file mode 100644 index 6b181a5af7bf3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx +++ /dev/null @@ -1,188 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; - -import React from 'react'; -import styled from 'styled-components'; -import { Description, Name, NotesButton, StarIcon } from './helpers'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; - -import { Note } from '../../../../common/lib/note'; -import { SuperDatePicker } from '../../../../common/components/super_date_picker'; -import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline'; - -import * as i18n from './translations'; -import { SaveTimelineButton } from '../header/save_timeline_button'; -import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants'; - -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; - -interface Props { - isFavorite: boolean; - timelineId: string; - timelineType: TimelineTypeLiteral; - updateIsFavorite: UpdateIsFavorite; - showDescription: boolean; - description: string; - title: string; - updateTitle: UpdateTitle; - updateDescription: UpdateDescription; - showNotes: boolean; - status: TimelineStatusLiteral; - associateNote: AssociateNote; - showNotesFromWidth: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - onToggleShowNotes: () => void; - noteIds: string[]; - updateNote: UpdateNote; - isDatepickerLocked: boolean; - toggleLock: () => void; - datePickerWidth: number; -} - -export const PropertiesLeftStyle = styled(EuiFlexGroup)` - width: 100%; -`; - -PropertiesLeftStyle.displayName = 'PropertiesLeftStyle'; - -export const LockIconContainer = styled(EuiFlexItem)` - margin-right: 2px; -`; - -LockIconContainer.displayName = 'LockIconContainer'; - -interface WidthProp { - width: number; -} - -export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ - style: { - width: `${width}px`, - }, -}))` - .euiSuperDatePicker__flexWrapper { - max-width: none; - width: auto; - } -`; - -DatePicker.displayName = 'DatePicker'; - -export const PropertiesLeft = React.memo( - ({ - isFavorite, - timelineId, - updateIsFavorite, - showDescription, - description, - title, - timelineType, - updateTitle, - updateDescription, - status, - showNotes, - showNotesFromWidth, - associateNote, - getNotesByIds, - noteIds, - onToggleShowNotes, - updateNote, - isDatepickerLocked, - toggleLock, - datePickerWidth, - }) => ( - - - - - - - - {showDescription ? ( - - - - ) : null} - - {ENABLE_NEW_TIMELINE && } - - {showNotesFromWidth ? ( - - - - ) : null} - - - - - - - - - - - - - - - ) -); - -PropertiesLeft.displayName = 'PropertiesLeft'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx index 3f02772b46bb3..7636822c0661f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx @@ -51,14 +51,12 @@ describe('Properties Right', () => { timelineId: 'timelineId', isDataInTimeline: false, showNotes: false, - showNotesFromWidth: false, showDescription: false, showUsersView: false, usersViewing: [], description: 'desc', updateDescription: jest.fn(), associateNote: jest.fn(), - getNotesByIds: jest.fn(), noteIds: [], onToggleShowNotes: jest.fn(), onCloseTimelineModal: jest.fn(), @@ -112,64 +110,6 @@ describe('Properties Right', () => { expect(wrapper.find('[data-test-subj="Description"]').exists()).not.toBeTruthy(); }); }); - - describe('render with notes button', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - const propsWithshowNotes = { - ...props, - showNotesFromWidth: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).toBeTruthy(); - }); - }); - - describe('render with description', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - const propsWithshowDescription = { - ...props, - showDescription: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).toBeTruthy(); - }); - }); }); describe('with no crud', () => { @@ -213,63 +153,5 @@ describe('Properties Right', () => { expect(wrapper.find('[data-test-subj="Description"]').exists()).not.toBeTruthy(); }); }); - - describe('render with notes button', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, - }, - }); - const propsWithshowNotes = { - ...props, - showNotesFromWidth: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).toBeTruthy(); - }); - }); - - describe('render with description', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, - }, - }); - const propsWithshowDescription = { - ...props, - showDescription: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).toBeTruthy(); - }); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx index 12eab4942128f..7364a83087f54 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, EuiAvatar, } from '@elastic/eui'; -import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers'; +import { NewTimeline, NewCase, ExistingCase } from './helpers'; import { TimelineStatusLiteral, @@ -22,9 +22,6 @@ import { TimelineType, } from '../../../../../common/types/timeline'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import { Note } from '../../../../common/lib/note'; - -import { AssociateNote } from '../../notes/helpers'; import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button'; import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; @@ -62,62 +59,39 @@ const Avatar = styled(EuiAvatar)` Avatar.displayName = 'Avatar'; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -export type UpdateNote = (note: Note) => void; - interface PropertiesRightComponentProps { - associateNote: AssociateNote; - description: string; - getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; isDataInTimeline: boolean; - noteIds: string[]; onButtonClick: () => void; onClosePopover: () => void; onCloseTimelineModal: () => void; onOpenCaseModal: () => void; onOpenTimelineModal: () => void; - onToggleShowNotes: () => void; showActions: boolean; - showDescription: boolean; - showNotes: boolean; - showNotesFromWidth: boolean; showTimelineModal: boolean; showUsersView: boolean; status: TimelineStatusLiteral; timelineId: string; title: string; timelineType: TimelineTypeLiteral; - updateDescription: UpdateDescription; - updateNote: UpdateNote; usersViewing: string[]; } const PropertiesRightComponent: React.FC = ({ - associateNote, - description, - getNotesByIds, graphEventId, isDataInTimeline, - noteIds, onButtonClick, onClosePopover, onCloseTimelineModal, onOpenCaseModal, onOpenTimelineModal, - onToggleShowNotes, showActions, - showDescription, - showNotes, - showNotesFromWidth, showTimelineModal, showUsersView, status, timelineType, timelineId, title, - updateDescription, - updateNote, usersViewing, }) => { return ( @@ -191,37 +165,6 @@ const PropertiesRightComponent: React.FC = ({ title={i18n.INSPECT_TIMELINE_TITLE} /> - - {showNotesFromWidth ? ( - - - - ) : null} - - {showDescription ? ( - - - - - - ) : null} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx index e4504d40bc0a7..be883fb20e6bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx @@ -78,12 +78,6 @@ export const LabelText = styled.div` `; LabelText.displayName = 'LabelText'; -export const StyledStar = styled(EuiIcon)` - margin-right: 5px; - cursor: pointer; -`; -StyledStar.displayName = 'StyledStar'; - export const Facet = styled.div` align-items: center; display: inline-flex; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts index 78d01b2d98ab3..1322e69baced5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts @@ -165,36 +165,6 @@ export const STREAM_LIVE = i18n.translate( } ); -export const LOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.lockDatePickerTooltip', - { - defaultMessage: - 'Disable syncing of date/time range between the currently viewed page and your timeline', - } -); - -export const UNLOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.unlockDatePickerTooltip', - { - defaultMessage: - 'Enable syncing of date/time range between the currently viewed page and your timeline', - } -); - -export const LOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( - 'xpack.securitySolution.timeline.properties.lockDatePickerDescription', - { - defaultMessage: 'Lock date picker to global date picker', - } -); - -export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( - 'xpack.securitySolution.timeline.properties.unlockDatePickerDescription', - { - defaultMessage: 'Unlock date picker to global date picker', - } -); - export const OPTIONAL = i18n.translate( 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index 78521d1c06248..ece7dbe7d18ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -5,17 +5,25 @@ */ import { + EuiIcon, + EuiSpacer, + EuiTitle, + EuiText, + EuiTabbedContent, EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiProgress, + EuiToolTip, + EuiButtonIcon, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; import styled from 'styled-components'; +import { ActionCreator } from 'typescript-fsa'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; import { BrowserFields, DocValueFields } from '../../../common/containers/source'; import { Direction, TimelineItem } from '../../../../common/search_strategy'; @@ -32,7 +40,7 @@ import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; -import { TIMELINE_TEMPLATE } from './translations'; +import * as i18n from './translations'; import { esQuery, Filter, @@ -48,6 +56,9 @@ import { ActiveTimelineExpandedEvent, } from '../../containers/active_timeline_context'; import { GraphOverlay } from '../graph_overlay'; +import { NotesTabContent } from '../notes'; +import { SuperDatePicker } from '../../../common/components/super_date_picker'; +import { InputsModelId } from '../../../common/store/inputs/constants'; const TimelineContainer = styled.div` height: 100%; @@ -109,6 +120,15 @@ const TimelineTemplateBadge = styled.div` font-size: 0.8em; `; +export const DatePicker = styled(EuiFlexItem)` + .euiSuperDatePicker__flexWrapper { + max-width: none; + width: auto; + } +`; + +DatePicker.displayName = 'DatePicker'; + export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -120,6 +140,7 @@ export interface Props { id: string; indexNames: string[]; indexPattern: IIndexPattern; + isDatePickerLocked: boolean; isLive: boolean; isSaving: boolean; itemsPerPage: number; @@ -127,6 +148,7 @@ export interface Props { kqlMode: KqlMode; kqlQueryExpression: string; loadingSourcerer: boolean; + noteIds: string[]; onChangeItemsPerPage: OnChangeItemsPerPage; onClose: () => void; show: boolean; @@ -136,7 +158,7 @@ export interface Props { status: TimelineStatusLiteral; timelineType: TimelineType; timerangeKind: 'absolute' | 'relative'; - toggleColumn: (column: ColumnHeaderOptions) => void; + toggleLock: ActionCreator<{ linkToId: InputsModelId }>; usersViewing: string[]; } @@ -152,6 +174,7 @@ export const TimelineComponent: React.FC = ({ id, indexPattern, indexNames, + isDatePickerLocked, isLive, loadingSourcerer, isSaving, @@ -159,6 +182,7 @@ export const TimelineComponent: React.FC = ({ itemsPerPageOptions, kqlMode, kqlQueryExpression, + noteIds, onChangeItemsPerPage, onClose, show, @@ -168,13 +192,15 @@ export const TimelineComponent: React.FC = ({ sort, timelineType, timerangeKind, - toggleColumn, + toggleLock, usersViewing, }) => { const [expanded, setExpanded] = useState( activeTimeline.getExpandedEvent() ); + const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); + const onEventToggled = useCallback((event: TimelineItem) => { const eventId = event._id; @@ -265,101 +291,182 @@ export const TimelineComponent: React.FC = ({ setIsQueryLoading(loading); }, [loading]); + const tabs = [ + { + id: 'cobalt--id', + name: 'Query', + content: ( + <> + + {canQueryTimeline ? ( + <> + + {graphEventId && ( + + )} + + +
+ {/* + */} + {/* */} + + + +
+
+ + + +
+ {/* */} + + + + + + + + + +