Skip to content

Commit

Permalink
[SIEM] Detection engine timeline (#53783) (#54318)
Browse files Browse the repository at this point in the history
* change create to only have only one form to be open at the same time

* add tick to risk score

* remove compressed

* fix select in schedule

* fix bug to not  allow more than one step panel to be open at a time

* Add a color/health indicator to severity selector

* Move and reword tags placeholder to bottom helper text

* fix ux on the index patterns field

* Reorganize MITRE ATT&CK threat

* add url validation + some cleaning to prerp work for UT

* add feature to get back timeline + be able to disable action on timeline modal

* Add option to import the query from a saved timeline.

* wip

* Add timeline template selector

* fix few bugs from last commit

* review I

* fix unit test for timeline_title

* ui review

* fix truncation on timeline selectable
  • Loading branch information
XavierM committed Jan 9, 2020
1 parent 36e2550 commit 0907022
Show file tree
Hide file tree
Showing 84 changed files with 2,169 additions and 679 deletions.
2 changes: 2 additions & 0 deletions src/plugins/es_ui_shared/static/forms/components/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
RadioGroupField,
RangeField,
SelectField,
SuperSelectField,
ToggleField,
} from './fields';

Expand All @@ -50,6 +51,7 @@ const mapTypeToFieldComponent = {
[FIELD_TYPES.RADIO_GROUP]: RadioGroupField,
[FIELD_TYPES.RANGE]: RangeField,
[FIELD_TYPES.SELECT]: SelectField,
[FIELD_TYPES.SUPER_SELECT]: SuperSelectField,
[FIELD_TYPES.TOGGLE]: ToggleField,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export * from './multi_select_field';
export * from './radio_group_field';
export * from './range_field';
export * from './select_field';
export * from './super_select_field';
export * from './toggle_field';
export * from './text_area_field';
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React from 'react';
import { EuiFormRow, EuiSuperSelect } from '@elastic/eui';

import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib';

interface Props {
field: FieldHook;
euiFieldProps?: Record<string, any>;
idAria?: string;
[key: string]: any;
}

export const SuperSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);

return (
<EuiFormRow
label={field.label}
helpText={field.helpText}
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
>
<EuiSuperSelect
fullWidth
valueOfSelected={field.value as string}
onChange={value => {
field.setValue(value);
}}
options={[]}
isInvalid={isInvalid}
data-test-subj="select"
{...euiFieldProps}
/>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const FIELD_TYPES = {
RADIO_GROUP: 'radioGroup',
RANGE: 'range',
SELECT: 'select',
SUPER_SELECT: 'superSelect',
MULTI_SELECT: 'multiSelect',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export interface QueryTimelineById<TCache> {
apolloClient: ApolloClient<TCache> | ApolloClient<{}> | undefined;
duplicate: boolean;
timelineId: string;
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
updateIsLoading: ActionCreator<{ id: string; isLoading: boolean }>;
updateTimeline: DispatchUpdateTimeline;
Expand All @@ -190,6 +191,7 @@ export const queryTimelineById = <TCache>({
apolloClient,
duplicate = false,
timelineId,
onOpenTimeline,
openTimeline = true,
updateIsLoading,
updateTimeline,
Expand All @@ -209,7 +211,9 @@ export const queryTimelineById = <TCache>({
);

const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate);
if (updateTimeline) {
if (onOpenTimeline != null) {
onOpenTimeline(timeline);
} else if (updateTimeline) {
updateTimeline({
duplicate,
from: getOr(getDefaultFromValue(), 'dateRange.start', timeline),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import { Dispatch } from 'redux';
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query';
import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all';

import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query';
import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types';
import { State, timelineSelectors } from '../../store';
import { timelineDefaults, TimelineModel } from '../../store/timeline/model';
import {
createTimeline as dispatchCreateNewTimeline,
updateIsLoading as dispatchUpdateIsLoading,
} from '../../store/timeline/actions';
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
import { OpenTimeline } from './open_timeline';
import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers';
import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body';
import {
ActionTimelineToShow,
DeleteTimelines,
EuiSearchBarQuery,
OnDeleteSelected,
Expand All @@ -41,14 +43,14 @@ import {
OpenTimelineReduxProps,
} from './types';
import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
import { ColumnHeader } from '../timeline/body/column_headers/column_header';
import { timelineDefaults } from '../../store/timeline/model';

interface OwnProps<TCache = object> {
apolloClient: ApolloClient<TCache>;
/** Displays open timeline in modal */
isModal: boolean;
closeModalTimeline?: () => void;
hideActions?: ActionTimelineToShow[];
onOpenTimeline?: (timeline: TimelineModel) => void;
}

export type OpenTimelineOwnProps = OwnProps &
Expand All @@ -69,15 +71,17 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str
/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */
export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
({
apolloClient,
closeModalTimeline,
createNewTimeline,
defaultPageSize,
hideActions = [],
isModal = false,
onOpenTimeline,
timeline,
title,
apolloClient,
closeModalTimeline,
updateTimeline,
updateIsLoading,
timeline,
createNewTimeline,
}) => {
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState<
Expand Down Expand Up @@ -212,6 +216,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
queryTimelineById({
apolloClient,
duplicate,
onOpenTimeline,
timelineId,
updateIsLoading,
updateTimeline,
Expand Down Expand Up @@ -286,6 +291,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
data-test-subj={'open-timeline-modal'}
deleteTimelines={onDeleteOneTimeline}
defaultPageSize={defaultPageSize}
hideActions={hideActions}
isLoading={loading}
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
onAddTimelinesToFavorites={undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('OpenTimeline', () => {
).toBe(true);
});

test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => {
test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline
Expand Down Expand Up @@ -178,10 +178,10 @@ describe('OpenTimeline', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(true);
expect(props.actionTimelineToShow).toContain('delete');
});

test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => {
test('it does NOT show the delete action columns when is onDeleteSelected undefined and deleteTimelines is specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline
Expand Down Expand Up @@ -215,10 +215,10 @@ describe('OpenTimeline', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => {
test('it does NOT show the delete action columns when is onDeleteSelected provided and deleteTimelines is undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline
Expand Down Expand Up @@ -252,10 +252,10 @@ describe('OpenTimeline', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => {
test('it does NOT show the delete action when both onDeleteSelected and deleteTimelines are undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline
Expand Down Expand Up @@ -288,6 +288,6 @@ describe('OpenTimeline', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
/>

<TimelinesTable
actionTimelineToShow={
onDeleteSelected != null && deleteTimelines != null
? ['delete', 'duplicate', 'selectable']
: ['duplicate', 'selectable']
}
data-test-subj="timelines-table"
deleteTimelines={deleteTimelines}
defaultPageSize={defaultPageSize}
Expand All @@ -69,7 +74,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
pageIndex={pageIndex}
pageSize={pageSize}
searchResults={searchResults}
showExtendedColumnsAndActions={onDeleteSelected != null && deleteTimelines != null}
showExtendedColumns={true}
sortDirection={sortDirection}
sortField={sortField}
totalSearchResultsCount={totalSearchResultsCount}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,49 @@
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import React from 'react';

import { TimelineModel } from '../../../store/timeline/model';
import { useApolloClient } from '../../../utils/apollo_context';

import * as i18n from '../translations';
import { ActionTimelineToShow } from '../types';
import { StatefulOpenTimeline } from '..';

export interface OpenTimelineModalProps {
onClose: () => void;
hideActions?: ActionTimelineToShow[];
modalTitle?: string;
onOpen?: (timeline: TimelineModel) => void;
}

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

export const OpenTimelineModal = React.memo<OpenTimelineModalProps>(({ onClose }) => {
const apolloClient = useApolloClient();

if (!apolloClient) return null;

return (
<EuiOverlayMask>
<EuiModal
data-test-subj="open-timeline-modal"
maxWidth={OPEN_TIMELINE_MODAL_WIDTH}
onClose={onClose}
>
<StatefulOpenTimeline
apolloClient={apolloClient}
closeModalTimeline={onClose}
isModal={true}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
title={i18n.OPEN_TIMELINE_TITLE}
/>
</EuiModal>
</EuiOverlayMask>
);
});
export const OpenTimelineModal = React.memo<OpenTimelineModalProps>(
({ hideActions = [], modalTitle, onClose, onOpen }) => {
const apolloClient = useApolloClient();

if (!apolloClient) return null;

return (
<EuiOverlayMask>
<EuiModal
data-test-subj="open-timeline-modal"
maxWidth={OPEN_TIMELINE_MODAL_WIDTH}
onClose={onClose}
>
<StatefulOpenTimeline
apolloClient={apolloClient}
closeModalTimeline={onClose}
hideActions={hideActions}
isModal={true}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
onOpenTimeline={onOpen}
title={modalTitle ?? i18n.OPEN_TIMELINE_TITLE}
/>
</EuiModal>
</EuiOverlayMask>
);
}
);

OpenTimelineModal.displayName = 'OpenTimelineModal';
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('OpenTimelineModal', () => {
).toBe(true);
});

test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => {
test('it shows the delete action when onDeleteSelected and deleteTimelines are specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModalBody
Expand Down Expand Up @@ -178,10 +178,10 @@ describe('OpenTimelineModal', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(true);
expect(props.actionTimelineToShow).toContain('delete');
});

test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => {
test('it does NOT show the delete when is onDeleteSelected undefined and deleteTimelines is specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModalBody
Expand Down Expand Up @@ -215,10 +215,10 @@ describe('OpenTimelineModal', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => {
test('it does NOT show the delete action when is onDeleteSelected provided and deleteTimelines is undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModalBody
Expand Down Expand Up @@ -252,10 +252,10 @@ describe('OpenTimelineModal', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});

test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => {
test('it does NOT show extended columns when both onDeleteSelected and deleteTimelines are undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModalBody
Expand Down Expand Up @@ -288,6 +288,6 @@ describe('OpenTimelineModal', () => {
.first()
.props() as TimelinesTableProps;

expect(props.showExtendedColumnsAndActions).toBe(false);
expect(props.actionTimelineToShow).not.toContain('delete');
});
});
Loading

0 comments on commit 0907022

Please sign in to comment.