mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SIEM] Fixes duplicate columns in timeline and fixes errors on multiple click on pinned event (#44010) (#44271)
* fix duplicate columns + error on multiple click on the pinned event * fix test * fix unpinned events * review + put back debounce for better experience * fix circulat dependency * review optional
This commit is contained in:
parent
c33cf805e4
commit
6d3e3eb2bc
11 changed files with 380 additions and 91 deletions
|
@ -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 } : {}),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* 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 } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
|
||||
|
@ -124,39 +123,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(
|
||||
<TestProviders>
|
||||
<Actions
|
||||
actionsColumnWidth={ACTIONS_COLUMN_WIDTH}
|
||||
associateNote={jest.fn()}
|
||||
checked={false}
|
||||
expanded={false}
|
||||
eventId="abc"
|
||||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={onPinClicked}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="pin-event"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(onPinClicked).toBeCalled();
|
||||
});
|
||||
|
||||
test('it invokes toggleShowNotes when the button for adding notes is clicked', () => {
|
||||
const toggleShowNotes = jest.fn();
|
||||
|
||||
|
@ -189,4 +155,37 @@ describe('Actions', () => {
|
|||
|
||||
expect(toggleShowNotes).toBeCalled();
|
||||
});
|
||||
|
||||
test('it invokes onPinClicked when the button for pinning events is clicked', () => {
|
||||
const onPinClicked = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Actions
|
||||
actionsColumnWidth={ACTIONS_COLUMN_WIDTH}
|
||||
associateNote={jest.fn()}
|
||||
checked={false}
|
||||
expanded={false}
|
||||
eventId="abc"
|
||||
eventIsPinned={false}
|
||||
getNotesByIds={jest.fn()}
|
||||
loading={false}
|
||||
noteIds={[]}
|
||||
onEventToggled={jest.fn()}
|
||||
onPinClicked={onPinClicked}
|
||||
showCheckboxes={false}
|
||||
showNotes={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="pin"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(onPinClicked).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* 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,
|
||||
EuiCheckbox,
|
||||
|
|
|
@ -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 { ActionTimeline, TimelineById } from './types';
|
||||
|
||||
interface TimelineEpicDependencies<State> {
|
||||
timelineByIdSelector: (state: State) => TimelineById;
|
||||
|
@ -75,15 +75,6 @@ interface TimelineEpicDependencies<State> {
|
|||
apolloClient$: Observable<AppApolloClient>;
|
||||
}
|
||||
|
||||
export interface ActionTimeline {
|
||||
type: string;
|
||||
payload: {
|
||||
id: string;
|
||||
eventId: string;
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const timelineActionsType = [
|
||||
applyKqlFilterQuery.type,
|
||||
addProvider.type,
|
||||
|
@ -158,7 +149,7 @@ export const createTimelineEpic = <State>(): 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 +170,25 @@ export const createTimelineEpic = <State>(): 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 +198,11 @@ export const createTimelineEpic = <State>(): 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$),
|
||||
|
|
|
@ -25,13 +25,13 @@ 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, TimelineById } from './types';
|
||||
|
||||
export const timelineFavoriteActionsType = [updateIsFavorite.type];
|
||||
|
||||
export const epicPersistTimelineFavorite = (
|
||||
apolloClient: ApolloClient<NormalizedCacheObject>,
|
||||
action: Action,
|
||||
action: ActionTimeline,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>
|
||||
|
@ -52,14 +52,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 +68,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$),
|
||||
|
|
|
@ -28,12 +28,13 @@ 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, TimelineById } from './types';
|
||||
|
||||
export const timelineNoteActionsType = [addNote.type, addNoteToEvent.type];
|
||||
|
||||
export const epicPersistNote = (
|
||||
apolloClient: ApolloClient<NormalizedCacheObject>,
|
||||
action: Action,
|
||||
action: ActionTimeline,
|
||||
timeline: TimelineById,
|
||||
notes: NotesById,
|
||||
action$: Observable<Action>,
|
||||
|
@ -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$),
|
||||
|
|
|
@ -26,13 +26,13 @@ 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, TimelineById } from './types';
|
||||
|
||||
export const timelinePinnedEventActionsType = [pinEvent.type, unPinEvent.type];
|
||||
|
||||
export const epicPersistPinnedEvent = (
|
||||
apolloClient: ApolloClient<NormalizedCacheObject>,
|
||||
action: Action,
|
||||
action: ActionTimeline,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>
|
||||
|
@ -47,14 +47,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 +59,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,29 +77,34 @@ export const epicPersistPinnedEvent = (
|
|||
savedTimeline.version == null && response.timelineVersion != null
|
||||
? response.timelineVersion
|
||||
: savedTimeline.version,
|
||||
pinnedEventIds: {
|
||||
...savedTimeline.pinnedEventIds,
|
||||
[action.payload.eventId]: 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(action.payload.eventId, 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$),
|
||||
|
|
270
x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts
Normal file
270
x-pack/legacy/plugins/siem/public/store/timeline/helpers.test.ts
Normal file
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 { Action } from 'redux';
|
||||
import { TimelineModel } from './model';
|
||||
|
||||
export interface AutoSavedWarningMsg {
|
||||
|
@ -24,3 +24,11 @@ export interface TimelineState {
|
|||
autoSavedWarningMsg: AutoSavedWarningMsg;
|
||||
showCallOutUnauthorizedMsg: boolean;
|
||||
}
|
||||
|
||||
export interface ActionTimeline extends Action<string> {
|
||||
payload: {
|
||||
id: string;
|
||||
eventId: string;
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -148,6 +148,13 @@ export class PinnedEvent {
|
|||
await this.deletePinnedEventOnTimeline(request, [pinnedEventId]);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (getOr(null, 'output.statusCode', err) === 404) {
|
||||
/*
|
||||
* Why we are doing that, because if it is not found for sure that it will be unpinned
|
||||
* There is no need to bring back this error since we can assume that it is unpinned
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
if (getOr(null, 'output.statusCode', err) === 403) {
|
||||
return pinnedEventId != null
|
||||
? {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue