Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Timeline] Fix timeline styling and createFrom beh… #72152

Merged
Merged
5 changes: 0 additions & 5 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,3 @@ export const showAllOthersBucket: string[] = [
'destination.ip',
'user.name',
];

/*
* This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged
*/
export const enableElasticFilter = false;
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const reformatDataProviderWithNewValue = <T extends DataProvider | DataPr
timelineType: TimelineType = TimelineType.default
): T => {
// Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields
if (timelineType === TimelineType.default) {
if (timelineType !== TimelineType.template) {
if (templateFields.includes(dataProvider.queryMatch.field)) {
const newValue = getStringArray(dataProvider.queryMatch.field, ecsData);
if (newValue.length) {
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/security_solution/public/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5654,6 +5654,8 @@ export namespace GetOneTimeline {

kqlQuery: Maybe<string>;

type: Maybe<DataProviderType>;

queryMatch: Maybe<_QueryMatch>;
};

Expand Down Expand Up @@ -5870,6 +5872,8 @@ export namespace PersistTimelineMutation {

eventType: Maybe<string>;

excludedRowRendererIds: Maybe<RowRendererId[]>;

favorite: Maybe<Favorite[]>;

filters: Maybe<Filters[]>;
Expand Down Expand Up @@ -5932,6 +5936,8 @@ export namespace PersistTimelineMutation {

kqlQuery: Maybe<string>;

type: Maybe<DataProviderType>;

queryMatch: Maybe<QueryMatch>;

and: Maybe<And[]>;
Expand Down Expand Up @@ -5964,6 +5970,8 @@ export namespace PersistTimelineMutation {

kqlQuery: Maybe<string>;

type: Maybe<DataProviderType>;

queryMatch: Maybe<_QueryMatch>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,203 @@ describe('helpers', () => {
width: 1100,
});
});

test('if duplicates and timeline.timelineType is not matching with outcome timelineType it should return draft with empty title', () => {
const timeline = {
savedObjectId: 'savedObject-1',
title: 'Awesome Timeline',
version: '1',
status: TimelineStatus.active,
timelineType: TimelineType.default,
};

const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template);
expect(newTimeline).toEqual({
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
width: 190,
},
{
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: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'savedObject-1',
isFavorite: false,
isLive: false,
isSelectAllChecked: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
kqlMode: 'filter',
kqlQuery: {
filterQuery: null,
filterQueryDraft: null,
},
loadingEventIds: [],
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
savedObjectId: 'savedObject-1',
selectedEventIds: {},
show: false,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
},
status: TimelineStatus.draft,
title: '',
timelineType: TimelineType.template,
templateTimelineId: null,
templateTimelineVersion: null,
version: '1',
width: 1100,
});
});

test('if duplicates and timeline.timelineType is not matching with outcome timelineType it should return draft with empty title template', () => {
const timeline = {
savedObjectId: 'savedObject-1',
title: 'Awesome Template',
version: '1',
status: TimelineStatus.active,
timelineType: TimelineType.template,
};

const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default);
expect(newTimeline).toEqual({
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
width: 190,
},
{
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: { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z' },
description: '',
deletedEventIds: [],
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
id: 'savedObject-1',
isFavorite: false,
isLive: false,
isSelectAllChecked: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],
kqlMode: 'filter',
kqlQuery: {
filterQuery: null,
filterQueryDraft: null,
},
loadingEventIds: [],
noteIds: [],
pinnedEventIds: {},
pinnedEventsSaveObject: {},
savedObjectId: 'savedObject-1',
selectedEventIds: {},
show: false,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
},
status: TimelineStatus.draft,
title: '',
timelineType: TimelineType.default,
templateTimelineId: null,
templateTimelineVersion: null,
version: '1',
width: 1100,
});
});

test('if columns are null, we should get the default columns', () => {
const timeline = {
savedObjectId: 'savedObject-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,29 +173,33 @@ const getTemplateTimelineId = (
duplicate: boolean,
targetTimelineType?: TimelineType
) => {
if (!duplicate) {
return timeline.templateTimelineId;
}

if (
targetTimelineType === TimelineType.default &&
timeline.timelineType === TimelineType.template
) {
return timeline.templateTimelineId;
}

// TODO: MOVE TO BACKEND
return uuid.v4();
return duplicate && timeline.timelineType === TimelineType.template
? // TODO: MOVE TO THE BACKEND
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I did it here was to fix templateTimelineId was not available until the request came back.
If we move this to server side may cause update failure if users updating timeline continuously in a short time. Means the follow up request might have sent before the first one come back with a templateTimelineId.

uuid.v4()
: timeline.templateTimelineId;
};

const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) =>
deepMerge(dataProvider, {
type: DataProviderType.default,
queryMatch: {
value:
dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value,
},
});
const convertToDefaultField = ({ and, ...dataProvider }: DataProviderResult) => {
if (dataProvider.type === DataProviderType.template) {
return deepMerge(dataProvider, {
type: DataProviderType.default,
enabled: dataProvider.queryMatch!.operator !== IS_OPERATOR,
queryMatch: {
value:
dataProvider.queryMatch!.operator === IS_OPERATOR ? '' : dataProvider.queryMatch!.value,
},
});
}

return dataProvider;
};

const getDataProviders = (
duplicate: boolean,
Expand All @@ -212,6 +216,28 @@ const getDataProviders = (
return dataProviders;
};

export const getTimelineTitle = (
timeline: TimelineResult,
duplicate: boolean,
timelineType?: TimelineType
) => {
const isCreateTimelineFromAction = timelineType && timeline.timelineType !== timelineType;
if (isCreateTimelineFromAction) return '';

return duplicate ? `${timeline.title} - Duplicate` : timeline.title || '';
};

export const getTimelineStatus = (
timeline: TimelineResult,
duplicate: boolean,
timelineType?: TimelineType
) => {
const isCreateTimelineFromAction = timelineType && timeline.timelineType !== timelineType;
if (isCreateTimelineFromAction) return TimelineStatus.draft;

return duplicate ? TimelineStatus.active : timeline.status;
};

// eslint-disable-next-line complexity
export const defaultTimelineToTimelineModel = (
timeline: TimelineResult,
Expand All @@ -234,11 +260,11 @@ export const defaultTimelineToTimelineModel = (
pinnedEventIds: setPinnedEventIds(duplicate, timeline.pinnedEventIds),
pinnedEventsSaveObject: setPinnedEventsSaveObject(duplicate, timeline.pinnedEventsSaveObject),
id: duplicate ? '' : timeline.savedObjectId,
status: duplicate ? TimelineStatus.active : timeline.status,
status: getTimelineStatus(timeline, duplicate, timelineType),
savedObjectId: duplicate ? null : timeline.savedObjectId,
version: duplicate ? null : timeline.version,
timelineType: timelineType ?? timeline.timelineType,
title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '',
title: getTimelineTitle(timeline, duplicate, timelineType),
templateTimelineId: getTemplateTimelineId(timeline, duplicate, timelineType),
templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiPanel, EuiBasicTable, EuiSpacer } from '@elastic/eui';
import { EuiPanel, EuiBasicTable } from '@elastic/eui';
import React, { useCallback, useMemo, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';

Expand Down Expand Up @@ -183,7 +183,6 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
/>

<EuiPanel className={OPEN_TIMELINE_CLASS_NAME}>
<EuiSpacer size="m" />
{!!timelineFilter && timelineFilter}
<SearchRow
data-test-subj="search-row"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface OpenTimelineModalProps {
}

const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px
const OPEN_TIMELINE_MODAL_WIDTH = 1100; // px

export const OpenTimelineModal = React.memo<OpenTimelineModalProps>(
({ hideActions = [], modalTitle, onClose, onOpen }) => {
Expand Down
Loading