From 40f946b533a254308de5667c4187623a61f37a7c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 26 Aug 2019 12:06:05 -0400 Subject: [PATCH 1/5] fix duplicate columns + error on multiple click on the pinned event --- .../timeline/body/actions/index.test.tsx | 70 ++--- .../timeline/body/actions/index.tsx | 4 +- .../public/store/timeline/helpers.test.ts | 270 ++++++++++++++++++ .../siem/public/store/timeline/helpers.ts | 16 +- 4 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx index 977deec6e0ece..0b4a61a915a6a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx @@ -124,39 +124,6 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="pin"]') - .first() - .simulate('click'); - - expect(onPinClicked).toBeCalled(); - }); - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { const toggleShowNotes = jest.fn(); @@ -189,4 +156,41 @@ describe('Actions', () => { expect(toggleShowNotes).toBeCalled(); }); + describe('mock debounce from lodash/fp', () => { + jest.mock('lodash/fp', () => ({ + debounce: (time: number, faker: () => void) => faker, + })); + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const onPinClicked = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="pin"]') + .first() + .simulate('click'); + + expect(onPinClicked).toBeCalled(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 210e030c56d59..70e057a84a55d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -12,7 +12,7 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; -import { noop } from 'lodash/fp'; +import { noop, debounce } from 'lodash/fp'; import * as React from 'react'; import styled from 'styled-components'; @@ -157,7 +157,7 @@ export const Actions = React.memo( allowUnpinning={!eventHasNotes(noteIds)} pinned={eventIsPinned} data-test-subj="pin-event" - onClick={onPinClicked} + onClick={debounce(600, onPinClicked)} /> diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts new file mode 100644 index 0000000000000..875f7ee8b6a4b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts @@ -0,0 +1,270 @@ +/* + * 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 { addTimelineToStore } from './helpers'; +import { TimelineResult } from '../../graphql/types'; +import { timelineDefaults } from './model'; + +describe('helpers', () => { + describe('#addTimelineToStore', () => { + test('if title is null, we should get the default title', () => { + const timeline: TimelineResult = { + savedObjectId: 'savedObject-1', + title: null, + version: '1', + }; + const newTimeline = addTimelineToStore({ id: 'timeline-1', timeline }); + expect(newTimeline).toEqual({ + 'timeline-1': { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 240, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 0, + start: 0, + }, + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + show: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + version: '1', + width: 1100, + }, + }); + }); + test('if columns are null, we should get the default columns', () => { + const timeline: TimelineResult = { + savedObjectId: 'savedObject-1', + columns: null, + version: '1', + }; + const newTimeline = addTimelineToStore({ id: 'timeline-1', timeline }); + expect(newTimeline).toEqual({ + 'timeline-1': { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 240, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 0, + start: 0, + }, + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + show: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + version: '1', + width: 1100, + }, + }); + }); + test('should merge columns when event.action is deleted without two extra column names of user.name', () => { + const timeline: TimelineResult = { + savedObjectId: 'savedObject-1', + columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), + version: '1', + }; + const newTimeline = addTimelineToStore({ id: 'timeline-1', timeline }); + expect(newTimeline).toEqual({ + 'timeline-1': { + savedObjectId: 'savedObject-1', + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 240, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + version: '1', + dataProviders: [], + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + title: '', + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + show: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + width: 1100, + id: 'savedObject-1', + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index 34759730d2563..541c0f24331ca 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getOr, omit, uniq, isEmpty, isEqualWith, defaultsDeep, pickBy, isNil } from 'lodash/fp'; +import { getOr, omit, uniq, isEmpty, isEqualWith, set } from 'lodash/fp'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { getColumnWidthFromType } from '../../components/timeline/body/helpers'; @@ -109,6 +109,18 @@ interface AddTimelineParams { timeline: TimelineResult; } +const mergeTimeline = (timeline: TimelineResult): TimelineModel => { + return Object.entries(timeline).reduce( + (acc: TimelineModel, [key, value]) => { + if (value != null) { + acc = set(key, value, acc); + } + return acc; + }, + { ...timelineDefaults, id: '' } + ); +}; + /** * Add a saved object timeline to the store * and default the value to what need to be if values are null @@ -116,7 +128,7 @@ interface AddTimelineParams { export const addTimelineToStore = ({ id, timeline }: AddTimelineParams): TimelineById => ({ // TODO: revisit this when we support multiple timelines [id]: { - ...defaultsDeep(timelineDefaults, pickBy(v => !isNil(v), timeline)), + ...mergeTimeline(timeline), id: timeline.savedObjectId || '', show: true, }, From 75540002478984792bd9be42afe06175382b3b3c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 26 Aug 2019 13:22:35 -0400 Subject: [PATCH 2/5] fix test --- .../timeline/body/actions/index.test.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx index 0b4a61a915a6a..277b44ba5df99 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { debounce } from 'lodash/fp'; import { mount } from 'enzyme'; import * as React from 'react'; @@ -12,6 +12,8 @@ import { ACTIONS_COLUMN_WIDTH } from '../helpers'; import { Actions } from '.'; +jest.mock('lodash/fp'); + describe('Actions', () => { test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { const wrapper = mount( @@ -157,9 +159,16 @@ describe('Actions', () => { expect(toggleShowNotes).toBeCalled(); }); describe('mock debounce from lodash/fp', () => { - jest.mock('lodash/fp', () => ({ - debounce: (time: number, faker: () => void) => faker, - })); + beforeEach(() => { + // @ts-ignore property mockImplementation does not exists + debounce.mockImplementation((time: number, fn: () => void) => { + return fn(); + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('it invokes onPinClicked when the button for pinning events is clicked', () => { const onPinClicked = jest.fn(); @@ -190,7 +199,7 @@ describe('Actions', () => { .first() .simulate('click'); - expect(onPinClicked).toBeCalled(); + expect(onPinClicked).toHaveBeenCalled(); }); }); }); From 444ff450d1f0afb00793a3e2e8314c9eadeef838 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 26 Aug 2019 23:09:45 -0400 Subject: [PATCH 3/5] fix unpinned events --- .../public/components/open_timeline/index.tsx | 5 +- .../timeline/body/actions/index.test.tsx | 81 ++++++++----------- .../timeline/body/actions/index.tsx | 4 +- .../store/timeline/epic_pinned_event.ts | 5 ++ .../server/lib/pinned_event/saved_object.ts | 3 + 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index fb3fc23f6985e..1aff9bf14a44b 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -418,7 +418,10 @@ export class StatefulOpenTimelineComponent extends React.PureComponent< ? {} : timelineModel.pinnedEventsSaveObject != null ? timelineModel.pinnedEventsSaveObject.reduce( - (acc, pinnedEvent) => ({ ...acc, [pinnedEvent.pinnedEventId]: pinnedEvent }), + (acc, pinnedEvent) => ({ + ...acc, + ...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}), + }), {} ) : {}, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx index 277b44ba5df99..977deec6e0ece 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { debounce } from 'lodash/fp'; + import { mount } from 'enzyme'; import * as React from 'react'; @@ -12,8 +12,6 @@ import { ACTIONS_COLUMN_WIDTH } from '../helpers'; import { Actions } from '.'; -jest.mock('lodash/fp'); - describe('Actions', () => { test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { const wrapper = mount( @@ -126,6 +124,39 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const onPinClicked = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="pin"]') + .first() + .simulate('click'); + + expect(onPinClicked).toBeCalled(); + }); + test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { const toggleShowNotes = jest.fn(); @@ -158,48 +189,4 @@ describe('Actions', () => { expect(toggleShowNotes).toBeCalled(); }); - describe('mock debounce from lodash/fp', () => { - beforeEach(() => { - // @ts-ignore property mockImplementation does not exists - debounce.mockImplementation((time: number, fn: () => void) => { - return fn(); - }); - }); - afterEach(() => { - jest.resetAllMocks(); - }); - - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="pin"]') - .first() - .simulate('click'); - - expect(onPinClicked).toHaveBeenCalled(); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 70e057a84a55d..210e030c56d59 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -12,7 +12,7 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; -import { noop, debounce } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import * as React from 'react'; import styled from 'styled-components'; @@ -157,7 +157,7 @@ export const Actions = React.memo( allowUnpinning={!eventHasNotes(noteIds)} pinned={eventIsPinned} data-test-subj="pin-event" - onClick={debounce(600, onPinClicked)} + onClick={onPinClicked} /> diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts index 8202c2f0dc49b..6bd92466e8fe9 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -80,6 +80,10 @@ export const epicPersistPinnedEvent = ( savedTimeline.version == null && response.timelineVersion != null ? response.timelineVersion : savedTimeline.version, + pinnedEventIds: { + ...savedTimeline.pinnedEventIds, + [get('payload.eventId', action)]: true, + }, pinnedEventsSaveObject: { ...savedTimeline.pinnedEventsSaveObject, [get('payload.eventId', action)]: response, @@ -90,6 +94,7 @@ export const epicPersistPinnedEvent = ( id: get('payload.id', action), timeline: { ...savedTimeline, + pinnedEventIds: omit(get('payload.eventId', action), savedTimeline.pinnedEventIds), pinnedEventsSaveObject: omit( get('payload.eventId', action), savedTimeline.pinnedEventsSaveObject diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts index 605acfdfbaae5..f6b154683e45c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -151,6 +151,9 @@ export class PinnedEvent { await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); return null; } catch (err) { + if (getOr(null, 'output.statusCode', err) === 404) { + return null; + } if (getOr(null, 'output.statusCode', err) === 403) { return pinnedEventId != null ? { From ec95d1c43bf3596a8b0ace51b1d78f6572f9aea7 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 27 Aug 2019 13:54:02 -0400 Subject: [PATCH 4/5] review + put back debounce for better experience --- .../timeline/body/actions/index.test.tsx | 81 +++++++++++-------- .../timeline/body/actions/index.tsx | 4 +- .../siem/public/store/timeline/epic.ts | 19 ++--- .../public/store/timeline/epic_favorite.ts | 11 +-- .../siem/public/store/timeline/epic_note.ts | 19 ++--- .../store/timeline/epic_pinned_event.ts | 33 +++----- 6 files changed, 86 insertions(+), 81 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx index 977deec6e0ece..277b44ba5df99 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { debounce } from 'lodash/fp'; import { mount } from 'enzyme'; import * as React from 'react'; @@ -12,6 +12,8 @@ import { ACTIONS_COLUMN_WIDTH } from '../helpers'; import { Actions } from '.'; +jest.mock('lodash/fp'); + describe('Actions', () => { test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { const wrapper = mount( @@ -124,39 +126,6 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="pin"]') - .first() - .simulate('click'); - - expect(onPinClicked).toBeCalled(); - }); - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { const toggleShowNotes = jest.fn(); @@ -189,4 +158,48 @@ describe('Actions', () => { expect(toggleShowNotes).toBeCalled(); }); + describe('mock debounce from lodash/fp', () => { + beforeEach(() => { + // @ts-ignore property mockImplementation does not exists + debounce.mockImplementation((time: number, fn: () => void) => { + return fn(); + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const onPinClicked = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="pin"]') + .first() + .simulate('click'); + + expect(onPinClicked).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 210e030c56d59..b80d5045fe414 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -12,7 +12,7 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; -import { noop } from 'lodash/fp'; +import { noop, debounce } from 'lodash/fp'; import * as React from 'react'; import styled from 'styled-components'; @@ -157,7 +157,7 @@ export const Actions = React.memo( allowUnpinning={!eventHasNotes(noteIds)} pinned={eventIsPinned} data-test-subj="pin-event" - onClick={onPinClicked} + onClick={debounce(300, onPinClicked)} /> diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index 2b31ac9505d18..1588c6525cfc6 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -75,7 +75,7 @@ interface TimelineEpicDependencies { apolloClient$: Observable; } -export interface ActionTimeline { +export interface ActionTimeline extends Action { type: string; payload: { id: string; @@ -158,7 +158,7 @@ export const createTimelineEpic = (): Epic< delay(500), withLatestFrom(timeline$, apolloClient$, notes$, timelineTimeRange$), concatMap(([objAction, timeline, apolloClient, notes, timelineTimeRange]) => { - const action: Action = get('action', objAction); + const action: ActionTimeline = get('action', objAction); const timelineId = myEpicTimelineId.getTimelineId(); const version = myEpicTimelineId.getTimelineVersion(); @@ -179,28 +179,25 @@ export const createTimelineEpic = (): Epic< variables: { timelineId, version, - timeline: convertTimelineAsInput( - timeline[get('payload.id', action)], - timelineTimeRange - ), + timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), }, refetchQueries, }) ).pipe( withLatestFrom(timeline$), mergeMap(([result, recentTimeline]) => { - const savedTimeline = recentTimeline[get('payload.id', action)]; + const savedTimeline = recentTimeline[action.payload.id]; const response: ResponseTimeline = get('data.persistTimeline', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ response.code === 409 ? updateAutoSaveMsg({ - timelineId: get('payload.id', action), + timelineId: action.payload.id, newTimelineModel: omitTypenameInTimeline(savedTimeline, response.timeline), }) : updateTimeline({ - id: get('payload.id', action), + id: action.payload.id, timeline: { ...savedTimeline, savedObjectId: response.timeline.savedObjectId, @@ -210,11 +207,11 @@ export const createTimelineEpic = (): Epic< }), ...callOutMsg, endTimelineSaving({ - id: get('payload.id', action), + id: action.payload.id, }), ]; }), - startWith(startTimelineSaving({ id: get('payload.id', action) })), + startWith(startTimelineSaving({ id: action.payload.id })), takeUntil( action$.pipe( withLatestFrom(timeline$), diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts index 62b54b959f215..de67751d8d44e 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts @@ -26,12 +26,13 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { TimelineById } from './types'; +import { ActionTimeline } from './epic'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; export const epicPersistTimelineFavorite = ( apolloClient: ApolloClient, - action: Action, + action: ActionTimeline, timeline: TimelineById, action$: Observable, timeline$: Observable @@ -52,14 +53,14 @@ export const epicPersistTimelineFavorite = ( ).pipe( withLatestFrom(timeline$), mergeMap(([result, recentTimelines]) => { - const savedTimeline = recentTimelines[get('payload.id', action)]; + const savedTimeline = recentTimelines[action.payload.id]; const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ ...callOutMsg, updateTimeline({ - id: get('payload.id', action), + id: action.payload.id, timeline: { ...savedTimeline, isFavorite: response.favorite != null && response.favorite.length > 0, @@ -68,11 +69,11 @@ export const epicPersistTimelineFavorite = ( }, }), endTimelineSaving({ - id: get('payload.id', action), + id: action.payload.id, }), ]; }), - startWith(startTimelineSaving({ id: get('payload.id', action) })), + startWith(startTimelineSaving({ id: action.payload.id })), takeUntil( action$.pipe( withLatestFrom(timeline$), diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts index 866617281c927..cd7e433274e28 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts @@ -29,11 +29,12 @@ import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { TimelineById } from './types'; +import { ActionTimeline } from './epic'; export const timelineNoteActionsType = [addNote.type, addNoteToEvent.type]; export const epicPersistNote = ( apolloClient: ApolloClient, - action: Action, + action: ActionTimeline, timeline: TimelineById, notes: NotesById, action$: Observable, @@ -52,8 +53,8 @@ export const epicPersistNote = ( noteId: null, version: null, note: { - eventId: get('payload.eventId', action), - note: getNote(get('payload.noteId', action), notes), + eventId: action.payload.eventId, + note: getNote(action.payload.noteId, notes), timelineId: myEpicTimelineId.getTimelineId(), }, }, @@ -62,17 +63,17 @@ export const epicPersistNote = ( ).pipe( withLatestFrom(timeline$, notes$), mergeMap(([result, recentTimeline, recentNotes]) => { - const noteIdRedux = get('payload.noteId', action); + const noteIdRedux = action.payload.noteId; const response: ResponseNote = get('data.persistNote', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ ...callOutMsg, - recentTimeline[get('payload.id', action)].savedObjectId == null + recentTimeline[action.payload.id].savedObjectId == null ? updateTimeline({ - id: get('payload.id', action), + id: action.payload.id, timeline: { - ...recentTimeline[get('payload.id', action)], + ...recentTimeline[action.payload.id], savedObjectId: response.note.timelineId || null, version: response.note.timelineVersion || null, }, @@ -94,11 +95,11 @@ export const epicPersistNote = ( }, }), endTimelineSaving({ - id: get('payload.id', action), + id: action.payload.id, }), ].filter(item => item != null); }), - startWith(startTimelineSaving({ id: get('payload.id', action) })), + startWith(startTimelineSaving({ id: action.payload.id })), takeUntil( action$.pipe( withLatestFrom(timeline$), diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts index 6bd92466e8fe9..42f2e5e8b1eea 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -27,12 +27,13 @@ import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { TimelineById } from './types'; +import { ActionTimeline } from './epic'; export const timelinePinnedEventActionsType = [pinEvent.type, unPinEvent.type]; export const epicPersistPinnedEvent = ( apolloClient: ApolloClient, - action: Action, + action: ActionTimeline, timeline: TimelineById, action$: Observable, timeline$: Observable @@ -47,14 +48,11 @@ export const epicPersistPinnedEvent = ( fetchPolicy: 'no-cache', variables: { pinnedEventId: - timeline[get('payload.id', action)].pinnedEventsSaveObject[ - get('payload.eventId', action) - ] != null - ? timeline[get('payload.id', action)].pinnedEventsSaveObject[ - get('payload.eventId', action) - ].pinnedEventId + timeline[action.payload.id].pinnedEventsSaveObject[action.payload.eventId] != null + ? timeline[action.payload.id].pinnedEventsSaveObject[action.payload.eventId] + .pinnedEventId : null, - eventId: get('payload.eventId', action), + eventId: action.payload.eventId, timelineId: myEpicTimelineId.getTimelineId(), }, refetchQueries, @@ -62,14 +60,14 @@ export const epicPersistPinnedEvent = ( ).pipe( withLatestFrom(timeline$), mergeMap(([result, recentTimeline]) => { - const savedTimeline = recentTimeline[get('payload.id', action)]; + const savedTimeline = recentTimeline[action.payload.id]; const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result); const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ response != null ? updateTimeline({ - id: get('payload.id', action), + id: action.payload.id, timeline: { ...savedTimeline, savedObjectId: @@ -80,34 +78,29 @@ export const epicPersistPinnedEvent = ( savedTimeline.version == null && response.timelineVersion != null ? response.timelineVersion : savedTimeline.version, - pinnedEventIds: { - ...savedTimeline.pinnedEventIds, - [get('payload.eventId', action)]: true, - }, pinnedEventsSaveObject: { ...savedTimeline.pinnedEventsSaveObject, - [get('payload.eventId', action)]: response, + [action.payload.eventId]: response, }, }, }) : updateTimeline({ - id: get('payload.id', action), + id: action.payload.id, timeline: { ...savedTimeline, - pinnedEventIds: omit(get('payload.eventId', action), savedTimeline.pinnedEventIds), pinnedEventsSaveObject: omit( - get('payload.eventId', action), + action.payload.eventId, savedTimeline.pinnedEventsSaveObject ), }, }), ...callOutMsg, endTimelineSaving({ - id: get('payload.id', action), + id: action.payload.id, }), ]; }), - startWith(startTimelineSaving({ id: get('payload.id', action) })), + startWith(startTimelineSaving({ id: action.payload.id })), takeUntil( action$.pipe( withLatestFrom(timeline$), From 45be987f3def88d2ceb9ed857acfe201f3473876 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Tue, 27 Aug 2019 14:07:02 -0600 Subject: [PATCH 5/5] Fix circle deps --- .../legacy/plugins/siem/public/store/timeline/epic.ts | 11 +---------- .../siem/public/store/timeline/epic_favorite.ts | 3 +-- .../plugins/siem/public/store/timeline/epic_note.ts | 3 +-- .../siem/public/store/timeline/epic_pinned_event.ts | 3 +-- .../plugins/siem/public/store/timeline/types.ts | 10 ++++++++++ 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index 1588c6525cfc6..e9f75bfabe70b 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -66,7 +66,7 @@ import { isNotNull } from './helpers'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; -import { TimelineById } from './types'; +import { TimelineById, ActionTimeline } from './types'; interface TimelineEpicDependencies { timelineByIdSelector: (state: State) => TimelineById; @@ -75,15 +75,6 @@ interface TimelineEpicDependencies { apolloClient$: Observable; } -export interface ActionTimeline extends Action { - type: string; - payload: { - id: string; - eventId: string; - noteId: string; - }; -} - const timelineActionsType = [ applyKqlFilterQuery.type, addProvider.type, diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts index de67751d8d44e..be9237e6a76b9 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts @@ -25,8 +25,7 @@ import { import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; -import { TimelineById } from './types'; -import { ActionTimeline } from './epic'; +import { TimelineById, ActionTimeline } from './types'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts index cd7e433274e28..8e84679822bd5 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts @@ -28,8 +28,7 @@ import { import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { TimelineById } from './types'; -import { ActionTimeline } from './epic'; +import { TimelineById, ActionTimeline } from './types'; export const timelineNoteActionsType = [addNote.type, addNoteToEvent.type]; export const epicPersistNote = ( diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts index 42f2e5e8b1eea..5ff8c09e05b87 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -26,8 +26,7 @@ import { import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { TimelineById } from './types'; -import { ActionTimeline } from './epic'; +import { TimelineById, ActionTimeline } from './types'; export const timelinePinnedEventActionsType = [pinEvent.type, unPinEvent.type]; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts b/x-pack/legacy/plugins/siem/public/store/timeline/types.ts index 5395df6941973..102d0b349cda4 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Action } from 'redux'; import { TimelineModel } from './model'; export interface AutoSavedWarningMsg { @@ -24,3 +25,12 @@ export interface TimelineState { autoSavedWarningMsg: AutoSavedWarningMsg; showCallOutUnauthorizedMsg: boolean; } + +export interface ActionTimeline extends Action { + type: string; + payload: { + id: string; + eventId: string; + noteId: string; + }; +}