[RAC] Persistent timeline fields fix (#110685)

* fix stringify circular ref crash and default columns on createTimeline

* rollback reset buton fix to split PR

* adding fields to the storage cleaning

* tests fixed

* test fix
This commit is contained in:
Sergi Massaneda 2021-09-02 16:33:07 +02:00 committed by GitHub
parent f21731fb3a
commit 56669c034c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 29 deletions

View file

@ -242,6 +242,9 @@ export const mockGlobalState: State = {
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.notes,
deletedEventIds: [],
documentType: '',
queryFields: [],
selectAll: false,
id: 'test',
savedObjectId: null,
columns: defaultHeaders,

View file

@ -1979,6 +1979,7 @@ export const mockTimelineModel: TimelineModel = {
},
deletedEventIds: [],
description: 'This is a sample rule description',
documentType: '',
eqlOptions: {
eventCategoryField: 'event.category',
tiebreakerField: 'event.sequence',
@ -2017,6 +2018,7 @@ export const mockTimelineModel: TimelineModel = {
kqlQuery: {
filterQuery: null,
},
queryFields: [],
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
loadingEventIds: [],
@ -2024,6 +2026,7 @@ export const mockTimelineModel: TimelineModel = {
pinnedEventIds: {},
pinnedEventsSaveObject: {},
savedObjectId: 'ef579e40-jibber-jabber',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -2110,6 +2113,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' },
deletedEventIds: [],
description: '',
documentType: '',
eqlOptions: {
eventCategoryField: 'event.category',
query: '',
@ -2141,7 +2145,9 @@ export const defaultTimelineProps: CreateTimelineProps = {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: null,
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,

View file

@ -148,6 +148,7 @@ describe('alert actions', () => {
},
deletedEventIds: [],
description: 'This is a sample rule description',
documentType: '',
eqlOptions: {
eventCategoryField: 'event.category',
query: '',
@ -204,7 +205,9 @@ describe('alert actions', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: null,
selectAll: false,
selectedEventIds: {},
show: true,
showCheckboxes: false,

View file

@ -296,6 +296,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -328,7 +329,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -366,6 +369,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -398,7 +402,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -436,6 +442,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -468,7 +475,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -504,6 +513,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -536,7 +546,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -577,6 +589,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -612,6 +625,8 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -680,6 +695,7 @@ describe('helpers', () => {
dateRange: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
dataProviders: [],
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -758,6 +774,8 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -791,6 +809,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { end: '2020-10-28T11:37:31.655Z', start: '2020-10-27T11:37:31.655Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -823,7 +842,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,
@ -861,6 +882,7 @@ describe('helpers', () => {
dataProviders: [],
dateRange: { end: '2020-07-08T08:20:18.966Z', start: '2020-07-07T08:20:18.966Z' },
description: '',
documentType: '',
deletedEventIds: [],
eqlOptions: {
eventCategoryField: 'event.category',
@ -893,7 +915,9 @@ describe('helpers', () => {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: 'savedObject-1',
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,

View file

@ -28,6 +28,17 @@ const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const getExpectedColumns = (model: TimelineModel) =>
model.columns.map(migrateColumnWidthToInitialWidth).map(migrateColumnLabelToDisplayAsText);
const {
documentType,
filterManager,
isLoading,
loadingText,
queryFields,
selectAll,
unit,
...timelineToStore
} = mockTimelineModel;
describe('SiemLocalStorage', () => {
const { localStorage, storage } = createSecuritySolutionStorageMock();
@ -41,7 +52,7 @@ describe('SiemLocalStorage', () => {
const timelineStorage = useTimelinesStorage();
timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel);
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
});
});
@ -50,8 +61,8 @@ describe('SiemLocalStorage', () => {
timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel);
timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel);
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageExternalAlerts]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
[TimelineId.hostsPageExternalAlerts]: timelineToStore,
});
});
});
@ -63,8 +74,8 @@ describe('SiemLocalStorage', () => {
timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel);
const timelines = timelineStorage.getAllTimelines();
expect(timelines).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageExternalAlerts]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
[TimelineId.hostsPageExternalAlerts]: timelineToStore,
});
});
@ -80,7 +91,7 @@ describe('SiemLocalStorage', () => {
const timelineStorage = useTimelinesStorage();
timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel);
const timeline = timelineStorage.getTimelineById(TimelineId.hostsPageEvents);
expect(timeline).toEqual(mockTimelineModel);
expect(timeline).toEqual(timelineToStore);
});
});
@ -94,8 +105,8 @@ describe('SiemLocalStorage', () => {
TimelineId.hostsPageExternalAlerts,
]);
expect(timelines).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageExternalAlerts]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
[TimelineId.hostsPageExternalAlerts]: timelineToStore,
});
});
@ -126,7 +137,7 @@ describe('SiemLocalStorage', () => {
TimelineId.hostsPageExternalAlerts,
]);
expect(timelines).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
});
});
@ -152,8 +163,8 @@ describe('SiemLocalStorage', () => {
// all legacy `width` values are migrated to `initialWidth`:
expect(timelines).toStrictEqual({
[TimelineId.hostsPageEvents]: {
...mockTimelineModel,
columns: mockTimelineModel.columns.map((c) => ({
...timelineToStore,
columns: timelineToStore.columns.map((c) => ({
...c,
displayAsText: undefined,
initialWidth: 98765,
@ -161,7 +172,7 @@ describe('SiemLocalStorage', () => {
})),
},
[TimelineId.hostsPageExternalAlerts]: {
...mockTimelineModel,
...timelineToStore,
columns: getExpectedColumns(mockTimelineModel),
},
});
@ -187,8 +198,8 @@ describe('SiemLocalStorage', () => {
expect(timelines).toStrictEqual({
[TimelineId.hostsPageEvents]: {
...mockTimelineModel,
columns: mockTimelineModel.columns.map((c) => ({
...timelineToStore,
columns: timelineToStore.columns.map((c) => ({
...c,
displayAsText: undefined,
initialWidth: c.initialWidth, // initialWidth is unchanged
@ -196,7 +207,7 @@ describe('SiemLocalStorage', () => {
})),
},
[TimelineId.hostsPageExternalAlerts]: {
...mockTimelineModel,
...timelineToStore,
columns: getExpectedColumns(mockTimelineModel),
},
});
@ -223,15 +234,15 @@ describe('SiemLocalStorage', () => {
// all legacy `label` values are migrated to `displayAsText`:
expect(timelines).toStrictEqual({
[TimelineId.hostsPageEvents]: {
...mockTimelineModel,
columns: mockTimelineModel.columns.map((c, i) => ({
...timelineToStore,
columns: timelineToStore.columns.map((c, i) => ({
...c,
displayAsText: `A legacy label ${i}`,
label: `A legacy label ${i}`,
})),
},
[TimelineId.hostsPageExternalAlerts]: {
...mockTimelineModel,
...timelineToStore,
columns: getExpectedColumns(mockTimelineModel),
},
});
@ -259,8 +270,8 @@ describe('SiemLocalStorage', () => {
expect(timelines).toStrictEqual({
[TimelineId.hostsPageEvents]: {
...mockTimelineModel,
columns: mockTimelineModel.columns.map((c, i) => ({
...timelineToStore,
columns: timelineToStore.columns.map((c, i) => ({
...c,
displayAsText:
'Label will NOT be migrated to displayAsText, because displayAsText already has a value',
@ -268,7 +279,7 @@ describe('SiemLocalStorage', () => {
})),
},
[TimelineId.hostsPageExternalAlerts]: {
...mockTimelineModel,
...timelineToStore,
columns: getExpectedColumns(mockTimelineModel),
},
});
@ -293,11 +304,11 @@ describe('SiemLocalStorage', () => {
expect(timelines).toStrictEqual({
[TimelineId.hostsPageEvents]: {
...mockTimelineModel,
...timelineToStore,
columns: 'this is NOT an array',
},
[TimelineId.hostsPageExternalAlerts]: {
...mockTimelineModel,
...timelineToStore,
columns: getExpectedColumns(mockTimelineModel),
},
});
@ -311,8 +322,8 @@ describe('SiemLocalStorage', () => {
timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel);
const timelines = getAllTimelinesInStorage(storage);
expect(timelines).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageExternalAlerts]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
[TimelineId.hostsPageExternalAlerts]: timelineToStore,
});
});
@ -326,7 +337,7 @@ describe('SiemLocalStorage', () => {
it('adds a timeline when storage is empty', () => {
addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel);
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
});
});
@ -334,8 +345,8 @@ describe('SiemLocalStorage', () => {
addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel);
addTimelineInStorage(storage, TimelineId.hostsPageExternalAlerts, mockTimelineModel);
expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({
[TimelineId.hostsPageEvents]: mockTimelineModel,
[TimelineId.hostsPageExternalAlerts]: mockTimelineModel,
[TimelineId.hostsPageEvents]: timelineToStore,
[TimelineId.hostsPageExternalAlerts]: timelineToStore,
});
});
});

View file

@ -85,13 +85,29 @@ export const addTimelineInStorage = (
id: TimelineIdLiteral,
timeline: TimelineModel
) => {
const timelineToStore = cleanStorageTimeline(timeline);
const timelines = getAllTimelinesInStorage(storage);
storage.set(LOCAL_STORAGE_TIMELINE_KEY, {
...timelines,
[id]: timeline,
[id]: timelineToStore,
});
};
const cleanStorageTimeline = (timeline: TimelineModel) => {
// discard unneeded fields to make sure the object serialization works
const {
documentType,
filterManager,
isLoading,
loadingText,
queryFields,
selectAll,
unit,
...timelineToStore
} = timeline;
return timelineToStore;
};
export const useTimelinesStorage = (): TimelinesStorage => {
const { storage } = useKibana().services;

View file

@ -19,6 +19,7 @@ export const timelineDefaults: SubsetTimelineModel &
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.query,
columns: defaultHeaders,
documentType: '',
defaultColumns: defaultHeaders,
dataProviders: [],
dateRange: { start, end },
@ -51,6 +52,7 @@ export const timelineDefaults: SubsetTimelineModel &
filterQuery: null,
},
loadingEventIds: [],
queryFields: [],
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
@ -59,6 +61,7 @@ export const timelineDefaults: SubsetTimelineModel &
pinnedEventIds: {},
pinnedEventsSaveObject: {},
savedObjectId: null,
selectAll: false,
selectedEventIds: {},
show: false,
showCheckboxes: false,

View file

@ -92,6 +92,7 @@ describe('Epic Timeline', () => {
],
deletedEventIds: [],
description: '',
documentType: '',
eqlOptions: {
eventCategoryField: 'event.category',
tiebreakerField: '',
@ -146,6 +147,8 @@ describe('Epic Timeline', () => {
},
},
loadingEventIds: [],
queryFields: [],
selectAll: false,
title: 'saved',
timelineType: TimelineType.default,
templateTimelineId: null,

View file

@ -85,10 +85,12 @@ export type SubsetTimelineModel = Readonly<
| 'dataProviders'
| 'deletedEventIds'
| 'description'
| 'documentType'
| 'eventType'
| 'eventIdToNoteIds'
| 'excludedRowRendererIds'
| 'expandedDetail'
| 'footerText'
| 'graphEventId'
| 'highlightedDropAndProviderId'
| 'historyIds'
@ -100,15 +102,18 @@ export type SubsetTimelineModel = Readonly<
| 'itemsPerPageOptions'
| 'kqlMode'
| 'kqlQuery'
| 'queryFields'
| 'title'
| 'timelineType'
| 'templateTimelineId'
| 'templateTimelineVersion'
| 'loadingEventIds'
| 'loadingText'
| 'noteIds'
| 'pinnedEventIds'
| 'pinnedEventsSaveObject'
| 'dateRange'
| 'selectAll'
| 'selectedEventIds'
| 'show'
| 'showCheckboxes'
@ -116,6 +121,7 @@ export type SubsetTimelineModel = Readonly<
| 'isSaving'
| 'isLoading'
| 'savedObjectId'
| 'unit'
| 'version'
| 'status'
>

View file

@ -88,6 +88,7 @@ const basicTimeline: TimelineModel = {
},
deletedEventIds: [],
description: '',
documentType: '',
eqlOptions: {
eventCategoryField: 'event.category',
tiebreakerField: '',
@ -113,7 +114,9 @@ const basicTimeline: TimelineModel = {
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
queryFields: [],
savedObjectId: null,
selectAll: false,
selectedEventIds: {},
show: true,
showCheckboxes: false,

View file

@ -92,11 +92,15 @@ export type TGridModelForTimeline = Pick<
| 'dataProviders'
| 'dateRange'
| 'deletedEventIds'
| 'documentType'
| 'excludedRowRendererIds'
| 'expandedDetail'
| 'filters'
| 'filterManager'
| 'footerText'
| 'graphEventId'
| 'kqlQuery'
| 'queryFields'
| 'id'
| 'indexNames'
| 'isLoading'
@ -104,11 +108,14 @@ export type TGridModelForTimeline = Pick<
| 'itemsPerPage'
| 'itemsPerPageOptions'
| 'loadingEventIds'
| 'loadingText'
| 'selectAll'
| 'showCheckboxes'
| 'sort'
| 'selectedEventIds'
| 'savedObjectId'
| 'title'
| 'unit'
| 'version'
>;