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

This commit is contained in:
Patryk Kopyciński 2020-07-20 16:14:13 +02:00 committed by GitHub
parent 6cf796a4fb
commit 7ac5fc4e1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 297 additions and 69 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -5654,6 +5654,8 @@ export namespace GetOneTimeline {
kqlQuery: Maybe<string>;
type: Maybe<DataProviderType>;
queryMatch: Maybe<_QueryMatch>;
};
@ -5870,6 +5872,8 @@ export namespace PersistTimelineMutation {
eventType: Maybe<string>;
excludedRowRendererIds: Maybe<RowRendererId[]>;
favorite: Maybe<Favorite[]>;
filters: Maybe<Filters[]>;
@ -5932,6 +5936,8 @@ export namespace PersistTimelineMutation {
kqlQuery: Maybe<string>;
type: Maybe<DataProviderType>;
queryMatch: Maybe<QueryMatch>;
and: Maybe<And[]>;
@ -5964,6 +5970,8 @@ export namespace PersistTimelineMutation {
kqlQuery: Maybe<string>;
type: Maybe<DataProviderType>;
queryMatch: Maybe<_QueryMatch>;
};

View file

@ -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',

View file

@ -173,10 +173,6 @@ const getTemplateTimelineId = (
duplicate: boolean,
targetTimelineType?: TimelineType
) => {
if (!duplicate) {
return timeline.templateTimelineId;
}
if (
targetTimelineType === TimelineType.default &&
timeline.timelineType === TimelineType.template
@ -184,18 +180,26 @@ const getTemplateTimelineId = (
return timeline.templateTimelineId;
}
// TODO: MOVE TO BACKEND
return uuid.v4();
return duplicate && timeline.timelineType === TimelineType.template
? // TODO: MOVE TO THE BACKEND
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,
@ -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,
@ -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,
};

View file

@ -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';
@ -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"

View file

@ -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 }) => {

View file

@ -79,43 +79,43 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
selectedTimelinesCount={selectedItems.length}
title={title}
/>
<>
<SearchRow
data-test-subj="search-row"
favoriteCount={favoriteCount}
onlyFavorites={onlyFavorites}
onQueryChange={onQueryChange}
onToggleOnlyFavorites={onToggleOnlyFavorites}
query=""
timelineType={timelineType}
>
{SearchRowContent}
</SearchRow>
</>
</HeaderContainer>
</EuiModalHeader>
<EuiModalBody>
<TimelinesTable
actionTimelineToShow={actionsToShow}
data-test-subj="timelines-table"
deleteTimelines={deleteTimelines}
defaultPageSize={defaultPageSize}
loading={isLoading}
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
onOpenTimeline={onOpenTimeline}
onSelectionChange={onSelectionChange}
onTableChange={onTableChange}
onToggleShowNotes={onToggleShowNotes}
pageIndex={pageIndex}
pageSize={pageSize}
searchResults={searchResults}
showExtendedColumns={false}
sortDirection={sortDirection}
sortField={sortField}
timelineType={timelineType}
totalSearchResultsCount={totalSearchResultsCount}
/>
<>
<SearchRow
data-test-subj="search-row"
favoriteCount={favoriteCount}
onlyFavorites={onlyFavorites}
onQueryChange={onQueryChange}
onToggleOnlyFavorites={onToggleOnlyFavorites}
query=""
timelineType={timelineType}
>
{SearchRowContent}
</SearchRow>
<TimelinesTable
actionTimelineToShow={actionsToShow}
data-test-subj="timelines-table"
deleteTimelines={deleteTimelines}
defaultPageSize={defaultPageSize}
loading={isLoading}
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
onOpenTimeline={onOpenTimeline}
onSelectionChange={onSelectionChange}
onTableChange={onTableChange}
onToggleShowNotes={onToggleShowNotes}
pageIndex={pageIndex}
pageSize={pageSize}
searchResults={searchResults}
showExtendedColumns={false}
sortDirection={sortDirection}
sortField={sortField}
timelineType={timelineType}
totalSearchResultsCount={totalSearchResultsCount}
/>
</>
</EuiModalBody>
</>
);

View file

@ -33,9 +33,7 @@ export const useTimelineStatus = ({
templateTimelineFilter: JSX.Element[] | null;
installPrepackagedTimelines: () => void;
} => {
const [selectedTab, setSelectedTab] = useState<TemplateTimelineTypeLiteralWithNull>(
TemplateTimelineType.elastic
);
const [selectedTab, setSelectedTab] = useState<TemplateTimelineTypeLiteralWithNull>(null);
const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [
timelineType,
]);

View file

@ -92,7 +92,7 @@ const ConvertFieldBadge = styled(ProviderFieldBadge)`
`;
const TemplateFieldBadge: React.FC<TemplateFieldBadgeProps> = ({ type, toggleType }) => {
if (type === DataProviderType.default) {
if (type !== DataProviderType.template) {
return (
<ConvertFieldBadge onClick={toggleType}>{i18n.CONVERT_TO_TEMPLATE_FIELD}</ConvertFieldBadge>
);

View file

@ -258,8 +258,7 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({
? `${width}px`
: '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
> button.euiButtonIcon,
> .euiToolTipAnchor > button.euiButtonIcon {
button.euiButtonIcon {
margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`};
}
`;

View file

@ -42,6 +42,7 @@ export const oneTimelineQuery = gql`
enabled
excluded
kqlQuery
type
queryMatch {
field
displayField

View file

@ -32,6 +32,7 @@ export const persistTimelineMutation = gql`
enabled
excluded
kqlQuery
type
queryMatch {
field
displayField
@ -45,6 +46,7 @@ export const persistTimelineMutation = gql`
enabled
excluded
kqlQuery
type
queryMatch {
field
displayField
@ -56,6 +58,7 @@ export const persistTimelineMutation = gql`
}
description
eventType
excludedRowRendererIds
favorite {
fullName
userName

View file

@ -68,6 +68,7 @@ import {
updateTimeline,
updateTitle,
updateAutoSaveMsg,
setExcludedRowRendererIds,
setFilters,
setSavedQueryId,
startTimelineSaving,
@ -88,9 +89,11 @@ import { ActionTimeline, TimelineEpicDependencies } from './types';
const timelineActionsType = [
applyKqlFilterQuery.type,
addProvider.type,
addTimeline.type,
dataProviderEdited.type,
removeColumn.type,
removeProvider.type,
setExcludedRowRendererIds.type,
setFilters.type,
setSavedQueryId.type,
updateColumns.type,

View file

@ -7,7 +7,7 @@
import { getOr } from 'lodash/fp';
import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
import { UNAUTHENTICATED_USER, enableElasticFilter } from '../../../common/constants';
import { UNAUTHENTICATED_USER } from '../../../common/constants';
import { NoteSavedObject } from '../../../common/types/timeline/note';
import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event';
import {
@ -153,12 +153,10 @@ const getTimelineTypeFilter = (
templateTimelineType == null
? null
: templateTimelineType === TemplateTimelineType.elastic
? `siem-ui-timeline.attributes.createdBy: "Elsatic"`
? `siem-ui-timeline.attributes.createdBy: "Elastic"`
: `not siem-ui-timeline.attributes.createdBy: "Elastic"`;
const filters = enableElasticFilter
? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]
: [typeFilter, draftFilter, immutableFilter];
const filters = [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter];
return filters.filter((f) => f != null).join(' and ');
};

View file

@ -114,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) {
enabled: true,
excluded: false,
kqlQuery: '',
type: 'default',
queryMatch: {
field: 'host.name',
displayField: null,