[SIEM] Fix auto save for template timeline (#65001)
* update save timeline * fix types * allow template timeline to be updated via import * fix unit tests * fix for review * handle update timeline Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
eae92e34c3
commit
537065a977
|
@ -144,6 +144,11 @@ export const TimelineTypeLiteralRt = runtimeTypes.union([
|
|||
runtimeTypes.literal(TimelineType.default),
|
||||
]);
|
||||
|
||||
const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt);
|
||||
|
||||
export type TimelineTypeLiteral = runtimeTypes.TypeOf<typeof TimelineTypeLiteralRt>;
|
||||
export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf<typeof TimelineTypeLiteralWithNullRt>;
|
||||
|
||||
export const SavedTimelineRuntimeType = runtimeTypes.partial({
|
||||
columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)),
|
||||
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
|
||||
|
|
|
@ -36,6 +36,7 @@ import { KueryFilterQueryKind } from '../../store/model';
|
|||
import { Note } from '../../lib/note';
|
||||
import moment from 'moment';
|
||||
import sinon from 'sinon';
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../store/inputs/actions');
|
||||
jest.mock('../../store/timeline/actions');
|
||||
|
@ -299,6 +300,9 @@ describe('helpers', () => {
|
|||
sortDirection: 'desc',
|
||||
},
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
version: '1',
|
||||
width: 1100,
|
||||
});
|
||||
|
@ -393,6 +397,9 @@ describe('helpers', () => {
|
|||
sortDirection: 'desc',
|
||||
},
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
version: '1',
|
||||
width: 1100,
|
||||
});
|
||||
|
@ -467,6 +474,9 @@ describe('helpers', () => {
|
|||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
|
@ -632,6 +642,9 @@ describe('helpers', () => {
|
|||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
|
|
|
@ -8,8 +8,8 @@ import ApolloClient from 'apollo-client';
|
|||
import { getOr, set, isEmpty } from 'lodash/fp';
|
||||
import { Action } from 'typescript-fsa';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query';
|
||||
import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types';
|
||||
import {
|
||||
|
@ -169,6 +169,8 @@ export const defaultTimelineToTimelineModel = (
|
|||
savedObjectId: duplicate ? null : timeline.savedObjectId,
|
||||
version: duplicate ? null : timeline.version,
|
||||
title: duplicate ? '' : timeline.title || '',
|
||||
templateTimelineId: duplicate ? null : timeline.templateTimelineId,
|
||||
templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion,
|
||||
}).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), {
|
||||
...timelineDefaults,
|
||||
id: '',
|
||||
|
|
|
@ -129,6 +129,9 @@ export const oneTimelineQuery = gql`
|
|||
version
|
||||
}
|
||||
title
|
||||
timelineType
|
||||
templateTimelineId
|
||||
templateTimelineVersion
|
||||
savedQueryId
|
||||
sort {
|
||||
columnId
|
||||
|
|
|
@ -5112,6 +5112,12 @@ export namespace GetOneTimeline {
|
|||
|
||||
title: Maybe<string>;
|
||||
|
||||
timelineType: Maybe<TimelineType>;
|
||||
|
||||
templateTimelineId: Maybe<string>;
|
||||
|
||||
templateTimelineVersion: Maybe<number>;
|
||||
|
||||
savedQueryId: Maybe<string>;
|
||||
|
||||
sort: Maybe<Sort>;
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
DEFAULT_INTERVAL_TYPE,
|
||||
DEFAULT_INTERVAL_VALUE,
|
||||
} from '../../common/constants';
|
||||
import { TimelineType } from '../../common/types/timeline';
|
||||
|
||||
export const mockGlobalState: State = {
|
||||
app: {
|
||||
|
@ -201,6 +202,9 @@ export const mockGlobalState: State = {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter';
|
||||
|
||||
import { TimelineType } from '../../common/types/timeline';
|
||||
|
||||
import { OpenTimelineResult } from '../components/open_timeline/types';
|
||||
import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../graphql/types';
|
||||
|
@ -10,7 +13,6 @@ import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query';
|
|||
import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types';
|
||||
import { TimelineModel } from '../store/timeline/model';
|
||||
import { timelineDefaults } from '../store/timeline/defaults';
|
||||
import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter';
|
||||
export interface MockedProvidedQuery {
|
||||
request: {
|
||||
query: GetAllTimeline.Query;
|
||||
|
@ -168,7 +170,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 1',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -297,7 +299,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 2',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -426,7 +428,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 2',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -555,7 +557,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 3',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -684,7 +686,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 4',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -813,7 +815,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 5',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -942,7 +944,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 6',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1071,7 +1073,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1200,7 +1202,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1329,7 +1331,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1458,7 +1460,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1587,7 +1589,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -1716,7 +1718,7 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [
|
|||
'ZF0W12oB9v5HJNSHwY6L',
|
||||
],
|
||||
title: 'test 7',
|
||||
timelineType: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
created: 1558386787614,
|
||||
|
@ -2141,6 +2143,9 @@ export const mockTimelineModel: TimelineModel = {
|
|||
sortDirection: Direction.desc,
|
||||
},
|
||||
title: 'Test rule',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
version: '1',
|
||||
width: 1100,
|
||||
};
|
||||
|
@ -2164,6 +2169,9 @@ export const mockTimelineResult: TimelineResult = {
|
|||
],
|
||||
kqlMode: 'filter',
|
||||
title: 'Test rule',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
savedQueryId: null,
|
||||
sort: { columnId: '@timestamp', sortDirection: 'desc' },
|
||||
version: '1',
|
||||
|
@ -2235,6 +2243,9 @@ export const defaultTimelineProps: CreateTimelineProps = {
|
|||
showRowRenderers: true,
|
||||
sort: { columnId: '@timestamp', sortDirection: Direction.desc },
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
version: null,
|
||||
width: 1100,
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '../../../../mock/';
|
||||
import { CreateTimeline, UpdateTimelineLoading } from './types';
|
||||
import { Ecs } from '../../../../graphql/types';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
jest.mock('apollo-client');
|
||||
|
||||
|
@ -215,6 +216,9 @@ describe('signals actions', () => {
|
|||
sortDirection: 'desc',
|
||||
},
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
version: null,
|
||||
width: 1100,
|
||||
},
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
import { Direction } from '../../graphql/types';
|
||||
import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants';
|
||||
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
|
||||
|
@ -33,6 +35,9 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
},
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
|
|
|
@ -4,12 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TimelineModel } from './model';
|
||||
import { Direction } from '../../graphql/types';
|
||||
import { convertTimelineAsInput } from './epic';
|
||||
|
||||
import { Filter, esFilters } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
import { Direction } from '../../graphql/types';
|
||||
|
||||
import { TimelineModel } from './model';
|
||||
import { convertTimelineAsInput } from './epic';
|
||||
|
||||
describe('Epic Timeline', () => {
|
||||
describe('#convertTimelineAsInput ', () => {
|
||||
test('should return a TimelineInput instead of TimelineModel ', () => {
|
||||
|
@ -135,6 +138,9 @@ describe('Epic Timeline', () => {
|
|||
},
|
||||
loadingEventIds: [],
|
||||
title: 'saved',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
|
@ -283,6 +289,9 @@ describe('Epic Timeline', () => {
|
|||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
},
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
timelineType: TimelineType.default,
|
||||
title: 'saved',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
} from 'rxjs/operators';
|
||||
|
||||
import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public';
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types';
|
||||
import { AppApolloClient } from '../../lib/lib';
|
||||
import { addError } from '../app/actions';
|
||||
|
@ -236,6 +237,9 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
...savedTimeline,
|
||||
savedObjectId: response.timeline.savedObjectId,
|
||||
version: response.timeline.version,
|
||||
timelineType: response.timeline.timelineType ?? TimelineType.default,
|
||||
templateTimelineId: response.timeline.templateTimelineId ?? null,
|
||||
templateTimelineVersion: response.timeline.templateTimelineVersion ?? null,
|
||||
isSaving: false,
|
||||
},
|
||||
}),
|
||||
|
@ -283,6 +287,9 @@ const timelineInput: TimelineInput = {
|
|||
kqlMode: null,
|
||||
kqlQuery: null,
|
||||
title: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
dateRange: null,
|
||||
savedQueryId: null,
|
||||
sort: null,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
|
||||
|
||||
import { Filter } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers';
|
||||
import { Sort } from '../../components/timeline/body/sort';
|
||||
import {
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
*/
|
||||
|
||||
import { Filter } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
import { TimelineTypeLiteralWithNull } from '../../../common/types/timeline';
|
||||
|
||||
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
|
||||
import { Sort } from '../../components/timeline/body/sort';
|
||||
import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types';
|
||||
|
@ -77,6 +80,12 @@ export interface TimelineModel {
|
|||
};
|
||||
/** Title */
|
||||
title: string;
|
||||
/** timelineTypes: default | template */
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
/** an unique id for template timeline */
|
||||
templateTimelineId: string | null;
|
||||
/** null for default timeline, number for template timeline */
|
||||
templateTimelineVersion: number | null;
|
||||
/** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */
|
||||
noteIds: string[];
|
||||
/** Events pinned to this timeline */
|
||||
|
@ -125,6 +134,9 @@ export type SubsetTimelineModel = Readonly<
|
|||
| 'kqlMode'
|
||||
| 'kqlQuery'
|
||||
| 'title'
|
||||
| 'timelineType'
|
||||
| 'templateTimelineId'
|
||||
| 'templateTimelineVersion'
|
||||
| 'loadingEventIds'
|
||||
| 'noteIds'
|
||||
| 'pinnedEventIds'
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import { cloneDeep, set } from 'lodash/fp';
|
||||
|
||||
import { TimelineType } from '../../../common/types/timeline';
|
||||
|
||||
import {
|
||||
IS_OPERATOR,
|
||||
DataProvider,
|
||||
|
@ -80,6 +82,9 @@ const timelineByIdMock: TimelineById = {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
pinnedEventIds: {},
|
||||
pinnedEventsSaveObject: {},
|
||||
|
@ -1110,6 +1115,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1202,6 +1210,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1400,6 +1411,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1492,6 +1506,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1679,6 +1696,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1755,6 +1775,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
@ -1855,6 +1878,9 @@ describe('Timeline', () => {
|
|||
kqlQuery: { filterQuery: null, filterQueryDraft: null },
|
||||
loadingEventIds: [],
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
noteIds: [],
|
||||
dateRange: {
|
||||
start: 0,
|
||||
|
|
|
@ -11,16 +11,21 @@ import { identity } from 'fp-ts/lib/function';
|
|||
import {
|
||||
TimelineSavedObjectRuntimeType,
|
||||
TimelineSavedObject,
|
||||
TimelineType,
|
||||
} from '../../../common/types/timeline';
|
||||
|
||||
export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => {
|
||||
const timeline = pipe(
|
||||
TimelineSavedObjectRuntimeType.decode(savedObject),
|
||||
map(savedTimeline => {
|
||||
const attributes = {
|
||||
...savedTimeline.attributes,
|
||||
timelineType: savedTimeline.attributes.timelineType ?? TimelineType.default,
|
||||
};
|
||||
return {
|
||||
savedObjectId: savedTimeline.id,
|
||||
version: savedTimeline.version,
|
||||
...savedTimeline.attributes,
|
||||
...attributes,
|
||||
};
|
||||
}),
|
||||
fold(errors => {
|
||||
|
|
|
@ -16,6 +16,7 @@ export const pickSavedTimeline = (
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): any => {
|
||||
const dateNow = new Date().valueOf();
|
||||
|
||||
if (timelineId == null) {
|
||||
savedTimeline.created = dateNow;
|
||||
savedTimeline.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER;
|
||||
|
@ -27,13 +28,15 @@ export const pickSavedTimeline = (
|
|||
}
|
||||
|
||||
if (savedTimeline.timelineType === TimelineType.template) {
|
||||
savedTimeline.timelineType = TimelineType.template;
|
||||
if (savedTimeline.templateTimelineId == null) {
|
||||
// create template timeline
|
||||
savedTimeline.templateTimelineId = uuid.v4();
|
||||
}
|
||||
|
||||
if (savedTimeline.templateTimelineVersion == null) {
|
||||
savedTimeline.templateTimelineVersion = 1;
|
||||
} else {
|
||||
// update template timeline
|
||||
if (savedTimeline.templateTimelineVersion != null) {
|
||||
savedTimeline.templateTimelineVersion = savedTimeline.templateTimelineVersion + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
savedTimeline.timelineType = TimelineType.default;
|
||||
|
|
|
@ -109,7 +109,7 @@ export const updateTemplateTimelineWithTimelineId = {
|
|||
timeline: {
|
||||
...inputTemplateTimeline,
|
||||
templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
templateTimelineVersion: 2,
|
||||
templateTimelineVersion: 1,
|
||||
},
|
||||
timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
|
||||
version: 'WzEyMjUsMV0=',
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('import timelines', () => {
|
|||
let securitySetup: SecurityPluginSetup;
|
||||
let { context } = requestContextMock.createTools();
|
||||
let mockGetTimeline: jest.Mock;
|
||||
let mockGetTemplateTimeline: jest.Mock;
|
||||
let mockPersistTimeline: jest.Mock;
|
||||
let mockPersistPinnedEventOnTimeline: jest.Mock;
|
||||
let mockPersistNote: jest.Mock;
|
||||
|
@ -51,6 +52,7 @@ describe('import timelines', () => {
|
|||
} as unknown) as SecurityPluginSetup;
|
||||
|
||||
mockGetTimeline = jest.fn();
|
||||
mockGetTemplateTimeline = jest.fn();
|
||||
mockPersistTimeline = jest.fn();
|
||||
mockPersistPinnedEventOnTimeline = jest.fn();
|
||||
mockPersistNote = jest.fn();
|
||||
|
@ -83,6 +85,7 @@ describe('import timelines', () => {
|
|||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(null),
|
||||
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline.mockReturnValue({
|
||||
timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion },
|
||||
}),
|
||||
|
@ -212,11 +215,12 @@ describe('import timelines', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Import a timeline already exist but overwrite is not allowed', () => {
|
||||
describe('Import a timeline already exist', () => {
|
||||
beforeEach(() => {
|
||||
jest.doMock('../saved_object', () => {
|
||||
return {
|
||||
getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
|
||||
getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
|
||||
persistTimeline: mockPersistTimeline,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -38,7 +38,9 @@ import {
|
|||
PromiseFromStreams,
|
||||
timelineSavedObjectOmittedFields,
|
||||
} from './utils/import_timelines';
|
||||
import { createTimelines, getTimeline } from './utils/create_timelines';
|
||||
import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
import { checkIsFailureCases } from './utils/update_timelines';
|
||||
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 10;
|
||||
|
||||
|
@ -121,6 +123,9 @@ export const importTimelinesRoute = (
|
|||
pinnedEventIds,
|
||||
globalNotes,
|
||||
eventNotes,
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
timelineType,
|
||||
} = parsedTimeline;
|
||||
const parsedTimelineObject = omit(
|
||||
timelineSavedObjectOmittedFields,
|
||||
|
@ -128,9 +133,23 @@ export const importTimelinesRoute = (
|
|||
);
|
||||
let newTimeline = null;
|
||||
try {
|
||||
const timeline = await getTimeline(frameworkRequest, savedObjectId);
|
||||
|
||||
if (timeline == null) {
|
||||
const templateTimeline =
|
||||
templateTimelineId != null
|
||||
? await getTemplateTimeline(frameworkRequest, templateTimelineId)
|
||||
: null;
|
||||
const timeline =
|
||||
templateTimeline?.savedObjectId != null || savedObjectId != null
|
||||
? await getTimeline(
|
||||
frameworkRequest,
|
||||
templateTimeline?.savedObjectId ?? savedObjectId
|
||||
)
|
||||
: null;
|
||||
const isHandlingTemplateTimeline = timelineType === TimelineType.template;
|
||||
if (
|
||||
(timeline == null && !isHandlingTemplateTimeline) ||
|
||||
(templateTimeline == null && isHandlingTemplateTimeline)
|
||||
) {
|
||||
// create timeline / template timeline
|
||||
newTimeline = await createTimelines(
|
||||
frameworkRequest,
|
||||
parsedTimelineObject,
|
||||
|
@ -141,6 +160,37 @@ export const importTimelinesRoute = (
|
|||
[] // existing note ids
|
||||
);
|
||||
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
});
|
||||
} else if (
|
||||
timeline != null &&
|
||||
templateTimeline != null &&
|
||||
isHandlingTemplateTimeline
|
||||
) {
|
||||
// update template timeline
|
||||
const errorObj = checkIsFailureCases(
|
||||
isHandlingTemplateTimeline,
|
||||
timeline.version,
|
||||
templateTimeline.templateTimelineVersion ?? null,
|
||||
timeline,
|
||||
templateTimeline
|
||||
);
|
||||
if (errorObj != null) {
|
||||
return siemResponse.error(errorObj);
|
||||
}
|
||||
|
||||
newTimeline = await createTimelines(
|
||||
frameworkRequest,
|
||||
{ ...parsedTimelineObject, templateTimelineId, templateTimelineVersion },
|
||||
timeline.savedObjectId, // timelineSavedObjectId
|
||||
timeline.version, // timelineVersion
|
||||
pinnedEventIds,
|
||||
globalNotes,
|
||||
[] // existing note ids
|
||||
);
|
||||
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
|
@ -150,7 +200,7 @@ export const importTimelinesRoute = (
|
|||
createBulkErrorObject({
|
||||
id: savedObjectId,
|
||||
statusCode: 409,
|
||||
message: `timeline_id: "${savedObjectId}" already exists`,
|
||||
message: `timeline_id: "${timeline?.savedObjectId}" already exists`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -215,6 +215,12 @@ describe('update timelines', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('should Update existing template timeline with timelineId', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][1]).toEqual(
|
||||
updateTemplateTimelineWithTimelineId.timelineId
|
||||
);
|
||||
});
|
||||
|
||||
test('should Update existing template timeline with timeline version', async () => {
|
||||
expect(mockPersistTimeline.mock.calls[0][2]).toEqual(
|
||||
updateTemplateTimelineWithTimelineId.version
|
||||
|
|
|
@ -96,4 +96,6 @@ export const timelineSavedObjectOmittedFields = [
|
|||
'createdBy',
|
||||
'updated',
|
||||
'updatedBy',
|
||||
'templateTimelineId',
|
||||
'templateTimelineVersion',
|
||||
];
|
||||
|
|
|
@ -14,8 +14,7 @@ export const NO_MATCH_VERSION_ERROR_MESSAGE =
|
|||
'TimelineVersion conflict: The given version doesn not match with existing timeline';
|
||||
export const NO_MATCH_ID_ERROR_MESSAGE =
|
||||
"Timeline id doesn't match with existing template timeline";
|
||||
export const OLDER_VERSION_ERROR_MESSAGE =
|
||||
'Template timelineVersion conflict: The given version is older then existing version';
|
||||
export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
|
||||
|
||||
export const checkIsFailureCases = (
|
||||
isHandlingTemplateTimeline: boolean,
|
||||
|
@ -68,11 +67,11 @@ export const checkIsFailureCases = (
|
|||
templateTimelineVersion != null &&
|
||||
existTemplateTimeline != null &&
|
||||
existTemplateTimeline.templateTimelineVersion != null &&
|
||||
existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion
|
||||
existTemplateTimeline.templateTimelineVersion !== templateTimelineVersion
|
||||
) {
|
||||
// Throw error you can not update a template timeline version with an old version
|
||||
return {
|
||||
body: OLDER_VERSION_ERROR_MESSAGE,
|
||||
body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
|
||||
statusCode: 409,
|
||||
};
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue