[Security Solution][Timeline] Fix timeline styling and createFrom beh… (#72152)
This commit is contained in:
parent
6cf796a4fb
commit
7ac5fc4e1f
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}`};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -42,6 +42,7 @@ export const oneTimelineQuery = gql`
|
|||
enabled
|
||||
excluded
|
||||
kqlQuery
|
||||
type
|
||||
queryMatch {
|
||||
field
|
||||
displayField
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ');
|
||||
};
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
enabled: true,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
type: 'default',
|
||||
queryMatch: {
|
||||
field: 'host.name',
|
||||
displayField: null,
|
||||
|
|
Loading…
Reference in a new issue