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;
+ };
+}