mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SIEM] Detection engine timeline (#53783)
* 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
This commit is contained in:
parent
404c42f955
commit
1e2cbb3710
84 changed files with 2174 additions and 684 deletions
|
@ -37,6 +37,7 @@ import {
|
|||
RadioGroupField,
|
||||
RangeField,
|
||||
SelectField,
|
||||
SuperSelectField,
|
||||
ToggleField,
|
||||
} from './fields';
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ export const FIELD_TYPES = {
|
|||
RADIO_GROUP: 'radioGroup',
|
||||
RANGE: 'range',
|
||||
SELECT: 'select',
|
||||
SUPER_SELECT: 'superSelect',
|
||||
MULTI_SELECT: 'multiSelect',
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -190,6 +191,7 @@ export const queryTimelineById = <TCache>({
|
|||
apolloClient,
|
||||
duplicate = false,
|
||||
timelineId,
|
||||
onOpenTimeline,
|
||||
openTimeline = true,
|
||||
updateIsLoading,
|
||||
updateTimeline,
|
||||
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
@ -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 &
|
||||
|
@ -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>(
|
||||
({
|
||||
defaultPageSize,
|
||||
isModal = false,
|
||||
title,
|
||||
apolloClient,
|
||||
closeModalTimeline,
|
||||
createNewTimeline,
|
||||
defaultPageSize,
|
||||
hideActions = [],
|
||||
isModal = false,
|
||||
onOpenTimeline,
|
||||
timeline,
|
||||
title,
|
||||
updateTimeline,
|
||||
updateIsLoading,
|
||||
timeline,
|
||||
createNewTimeline,
|
||||
}) => {
|
||||
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
|
||||
const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState<
|
||||
|
@ -212,6 +216,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
queryTimelineById({
|
||||
apolloClient,
|
||||
duplicate,
|
||||
onOpenTimeline,
|
||||
timelineId,
|
||||
updateIsLoading,
|
||||
updateTimeline,
|
||||
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -288,6 +288,6 @@ describe('OpenTimeline', () => {
|
|||
.first()
|
||||
.props() as TimelinesTableProps;
|
||||
|
||||
expect(props.showExtendedColumnsAndActions).toBe(false);
|
||||
expect(props.actionTimelineToShow).not.toContain('delete');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
export const OpenTimelineModal = React.memo<OpenTimelineModalProps>(
|
||||
({ hideActions = [], modalTitle, onClose, onOpen }) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
if (!apolloClient) return null;
|
||||
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>
|
||||
);
|
||||
});
|
||||
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';
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -288,6 +288,6 @@ describe('OpenTimelineModal', () => {
|
|||
.first()
|
||||
.props() as TimelinesTableProps;
|
||||
|
||||
expect(props.showExtendedColumnsAndActions).toBe(false);
|
||||
expect(props.actionTimelineToShow).not.toContain('delete');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
|
||||
import { EuiModalBody, EuiModalHeader } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { OpenTimelineProps } from '../types';
|
||||
import { OpenTimelineProps, ActionTimelineToShow } from '../types';
|
||||
import { SearchRow } from '../search_row';
|
||||
import { TimelinesTable } from '../timelines_table';
|
||||
import { TitleRow } from '../title_row';
|
||||
|
@ -19,10 +19,11 @@ export const HeaderContainer = styled.div`
|
|||
|
||||
HeaderContainer.displayName = 'HeaderContainer';
|
||||
|
||||
export const OpenTimelineModalBody = React.memo<OpenTimelineProps>(
|
||||
export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
||||
({
|
||||
deleteTimelines,
|
||||
defaultPageSize,
|
||||
hideActions = [],
|
||||
isLoading,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
onAddTimelinesToFavorites,
|
||||
|
@ -43,51 +44,61 @@ export const OpenTimelineModalBody = React.memo<OpenTimelineProps>(
|
|||
sortField,
|
||||
title,
|
||||
totalSearchResultsCount,
|
||||
}) => (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<HeaderContainer>
|
||||
<TitleRow
|
||||
data-test-subj="title-row"
|
||||
onDeleteSelected={onDeleteSelected}
|
||||
onAddTimelinesToFavorites={onAddTimelinesToFavorites}
|
||||
selectedTimelinesCount={selectedItems.length}
|
||||
title={title}
|
||||
/>
|
||||
}) => {
|
||||
const actionsToShow = useMemo(() => {
|
||||
const actions: ActionTimelineToShow[] =
|
||||
onDeleteSelected != null && deleteTimelines != null
|
||||
? ['delete', 'duplicate']
|
||||
: ['duplicate'];
|
||||
return actions.filter(action => !hideActions.includes(action));
|
||||
}, [onDeleteSelected, deleteTimelines, hideActions]);
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<HeaderContainer>
|
||||
<TitleRow
|
||||
data-test-subj="title-row"
|
||||
onDeleteSelected={onDeleteSelected}
|
||||
onAddTimelinesToFavorites={onAddTimelinesToFavorites}
|
||||
selectedTimelinesCount={selectedItems.length}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
<SearchRow
|
||||
data-test-subj="search-row"
|
||||
onlyFavorites={onlyFavorites}
|
||||
onQueryChange={onQueryChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
query={query}
|
||||
<SearchRow
|
||||
data-test-subj="search-row"
|
||||
onlyFavorites={onlyFavorites}
|
||||
onQueryChange={onQueryChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
query={query}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
</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}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
</HeaderContainer>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<TimelinesTable
|
||||
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}
|
||||
showExtendedColumnsAndActions={onDeleteSelected != null && deleteTimelines != null}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
</>
|
||||
)
|
||||
</EuiModalBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
OpenTimelineModalBody.displayName = 'OpenTimelineModalBody';
|
||||
|
|
|
@ -27,10 +27,11 @@ describe('#getActionsColumns', () => {
|
|||
mockResults = cloneDeep(mockTimelineResults);
|
||||
});
|
||||
|
||||
test('it renders the delete timeline (trash icon) when showDeleteAction is true (because showExtendedColumnsAndActions is true)', () => {
|
||||
test('it renders the delete timeline (trash icon) when actionTimelineToShow is including the action delete', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -42,7 +43,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -53,10 +54,11 @@ describe('#getActionsColumns', () => {
|
|||
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT render the delete timeline (trash icon) when showDeleteAction is false (because showExtendedColumnsAndActions is false)', () => {
|
||||
test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow is NOT including the action delete', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={[]}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -68,7 +70,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -79,10 +81,12 @@ describe('#getActionsColumns', () => {
|
|||
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it does NOT render the delete timeline (trash icon) when deleteTimelines is not provided', () => {
|
||||
test('it renders the duplicate icon timeline when actionTimelineToShow is including the action duplicate', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['duplicate']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
itemIdToExpandedNotesRowMap={{}}
|
||||
|
@ -93,7 +97,60 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT render the duplicate timeline when actionTimelineToShow is NOT including the action duplicate)', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={[]}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
itemIdToExpandedNotesRowMap={{}}
|
||||
onOpenTimeline={jest.fn()}
|
||||
onSelectionChange={jest.fn()}
|
||||
onTableChange={jest.fn()}
|
||||
onToggleShowNotes={jest.fn()}
|
||||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it does NOT render the delete timeline (trash icon) when deleteTimelines is not provided', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete']}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
itemIdToExpandedNotesRowMap={{}}
|
||||
onOpenTimeline={jest.fn()}
|
||||
onSelectionChange={jest.fn()}
|
||||
onTableChange={jest.fn()}
|
||||
onToggleShowNotes={jest.fn()}
|
||||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -111,6 +168,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -122,7 +180,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingSavedObjectId.length}
|
||||
|
@ -141,6 +199,7 @@ describe('#getActionsColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -152,7 +211,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -174,6 +233,7 @@ describe('#getActionsColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -185,7 +245,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
|
|
@ -12,19 +12,24 @@ import React from 'react';
|
|||
import { ACTION_COLUMN_WIDTH } from './common_styles';
|
||||
import { DeleteTimelineModalButton } from '../delete_timeline_modal';
|
||||
import * as i18n from '../translations';
|
||||
import { DeleteTimelines, OnOpenTimeline, OpenTimelineResult } from '../types';
|
||||
import {
|
||||
ActionTimelineToShow,
|
||||
DeleteTimelines,
|
||||
OnOpenTimeline,
|
||||
OpenTimelineResult,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Returns the action columns (e.g. delete, open duplicate timeline)
|
||||
*/
|
||||
export const getActionsColumns = ({
|
||||
actionTimelineToShow,
|
||||
onOpenTimeline,
|
||||
deleteTimelines,
|
||||
showDeleteAction,
|
||||
}: {
|
||||
actionTimelineToShow: ActionTimelineToShow[];
|
||||
deleteTimelines?: DeleteTimelines;
|
||||
onOpenTimeline: OnOpenTimeline;
|
||||
showDeleteAction: boolean;
|
||||
}) => {
|
||||
const openAsDuplicateColumn = {
|
||||
align: 'center',
|
||||
|
@ -67,7 +72,10 @@ export const getActionsColumns = ({
|
|||
width: ACTION_COLUMN_WIDTH,
|
||||
};
|
||||
|
||||
return showDeleteAction && deleteTimelines != null
|
||||
? [openAsDuplicateColumn, deleteTimelineColumn]
|
||||
: [openAsDuplicateColumn];
|
||||
return [
|
||||
actionTimelineToShow.includes('duplicate') ? openAsDuplicateColumn : null,
|
||||
actionTimelineToShow.includes('delete') && deleteTimelines != null
|
||||
? deleteTimelineColumn
|
||||
: null,
|
||||
].filter(action => action != null);
|
||||
};
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -48,7 +49,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={hasNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={hasNotes.length}
|
||||
|
@ -63,6 +64,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -74,7 +76,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingNotes.length}
|
||||
|
@ -89,6 +91,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -100,7 +103,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={nullNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={nullNotes.length}
|
||||
|
@ -115,6 +118,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -126,7 +130,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={emptylNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={emptylNotes.length}
|
||||
|
@ -143,6 +147,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -154,7 +159,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingSavedObjectId.length}
|
||||
|
@ -169,6 +174,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -180,7 +186,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={nullSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={nullSavedObjectId.length}
|
||||
|
@ -195,6 +201,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -206,7 +213,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={hasNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={hasNotes.length}
|
||||
|
@ -231,6 +238,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -242,7 +250,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={hasNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={hasNotes.length}
|
||||
|
@ -269,6 +277,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -280,7 +289,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={hasNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={hasNotes.length}
|
||||
|
@ -311,6 +320,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -322,7 +332,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={hasNotes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={hasNotes.length}
|
||||
|
@ -346,6 +356,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -357,7 +368,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -377,6 +388,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -388,7 +400,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -411,6 +423,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -422,7 +435,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingSavedObjectId.length}
|
||||
|
@ -442,6 +455,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -453,7 +467,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingTitle}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingTitle.length}
|
||||
|
@ -475,6 +489,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -486,7 +501,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={withMissingSavedObjectIdAndTitle}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={withMissingSavedObjectIdAndTitle.length}
|
||||
|
@ -508,6 +523,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -519,7 +535,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={withJustWhitespaceTitle}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={withJustWhitespaceTitle.length}
|
||||
|
@ -541,6 +557,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -552,7 +569,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={withMissingSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={withMissingSavedObjectId.length}
|
||||
|
@ -571,6 +588,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -582,7 +600,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -605,6 +623,7 @@ describe('#getCommonColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -616,7 +635,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingSavedObjectId}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingSavedObjectId.length}
|
||||
|
@ -637,6 +656,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -648,7 +668,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -673,6 +693,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -684,7 +705,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -704,6 +725,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -715,7 +737,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -737,6 +759,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -748,7 +771,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingDescription}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingDescription.length}
|
||||
|
@ -771,6 +794,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -782,7 +806,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={justWhitespaceDescription}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={justWhitespaceDescription.length}
|
||||
|
@ -803,6 +827,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -814,7 +839,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -834,6 +859,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -845,7 +871,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -868,6 +894,7 @@ describe('#getCommonColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -879,7 +906,7 @@ describe('#getCommonColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingUpdated}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingUpdated.length}
|
||||
|
|
|
@ -27,11 +27,9 @@ export const getCommonColumns = ({
|
|||
itemIdToExpandedNotesRowMap,
|
||||
onOpenTimeline,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumnsAndActions,
|
||||
}: {
|
||||
onOpenTimeline: OnOpenTimeline;
|
||||
onToggleShowNotes: OnToggleShowNotes;
|
||||
showExtendedColumnsAndActions: boolean;
|
||||
itemIdToExpandedNotesRowMap: Record<string, JSX.Element>;
|
||||
}) => [
|
||||
{
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('#getExtendedColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -46,7 +47,7 @@ describe('#getExtendedColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -66,6 +67,7 @@ describe('#getExtendedColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -77,7 +79,7 @@ describe('#getExtendedColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -99,6 +101,7 @@ describe('#getExtendedColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -110,7 +113,7 @@ describe('#getExtendedColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={missingUpdatedBy}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={missingUpdatedBy.length}
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('#getActionsColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -41,7 +42,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -57,6 +58,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -68,7 +70,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={with6Events}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={with6Events.length}
|
||||
|
@ -82,6 +84,7 @@ describe('#getActionsColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -93,7 +96,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -109,6 +112,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -120,7 +124,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={with4Notes}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={with4Notes.length}
|
||||
|
@ -134,6 +138,7 @@ describe('#getActionsColumns', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -145,7 +150,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -161,6 +166,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -172,7 +178,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={undefinedFavorite}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={undefinedFavorite.length}
|
||||
|
@ -187,6 +193,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -198,7 +205,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={nullFavorite}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={nullFavorite.length}
|
||||
|
@ -213,6 +220,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -224,7 +232,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={emptyFavorite}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={emptyFavorite.length}
|
||||
|
@ -249,6 +257,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -260,7 +269,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={emptyFavorite}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={emptyFavorite.length}
|
||||
|
@ -289,6 +298,7 @@ describe('#getActionsColumns', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -300,7 +310,7 @@ describe('#getActionsColumns', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={emptyFavorite}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={emptyFavorite.length}
|
||||
|
|
|
@ -28,10 +28,11 @@ describe('TimelinesTable', () => {
|
|||
mockResults = cloneDeep(mockTimelineResults);
|
||||
});
|
||||
|
||||
test('it renders the select all timelines header checkbox when showExtendedColumnsAndActions is true', () => {
|
||||
test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -43,7 +44,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -59,10 +60,11 @@ describe('TimelinesTable', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT render the select all timelines header checkbox when showExtendedColumnsAndActions is false', () => {
|
||||
test('it does NOT render the select all timelines header checkbox when actionTimelineToShow has not the action selectable', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -74,7 +76,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -90,10 +92,11 @@ describe('TimelinesTable', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders the Modified By column when showExtendedColumnsAndActions is true ', () => {
|
||||
test('it renders the Modified By column when showExtendedColumns is true ', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -105,7 +108,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -121,10 +124,11 @@ describe('TimelinesTable', () => {
|
|||
).toContain(i18n.MODIFIED_BY);
|
||||
});
|
||||
|
||||
test('it renders the notes column in the position of the Modified By column when showExtendedColumnsAndActions is false', () => {
|
||||
test('it renders the notes column in the position of the Modified By column when showExtendedColumns is false', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -136,7 +140,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -148,16 +152,17 @@ describe('TimelinesTable', () => {
|
|||
wrapper
|
||||
.find('thead tr th')
|
||||
.at(5)
|
||||
.find('[data-test-subj="notes-count-header-icon"]')
|
||||
.find('svg[data-test-subj="notes-count-header-icon"]')
|
||||
.first()
|
||||
.exists()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders the delete timeline (trash icon) when showExtendedColumnsAndActions is true', () => {
|
||||
test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -169,7 +174,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -185,10 +190,11 @@ describe('TimelinesTable', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT render the delete timeline (trash icon) when showExtendedColumnsAndActions is false', () => {
|
||||
test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow has NOT the delete action', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -200,7 +206,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -216,10 +222,11 @@ describe('TimelinesTable', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders the rows per page selector when showExtendedColumnsAndActions is true', () => {
|
||||
test('it renders the rows per page selector when showExtendedColumns is true', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -231,7 +238,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -247,10 +254,11 @@ describe('TimelinesTable', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT render the rows per page selector when showExtendedColumnsAndActions is false', () => {
|
||||
test('it does NOT render the rows per page selector when showExtendedColumns is false', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -262,7 +270,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -284,6 +292,7 @@ describe('TimelinesTable', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={defaultPageSize}
|
||||
loading={false}
|
||||
|
@ -295,7 +304,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={defaultPageSize}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -311,10 +320,11 @@ describe('TimelinesTable', () => {
|
|||
).toEqual('Rows per page: 123');
|
||||
});
|
||||
|
||||
test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is true ', () => {
|
||||
test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -326,7 +336,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -342,10 +352,11 @@ describe('TimelinesTable', () => {
|
|||
).toContain(i18n.LAST_MODIFIED);
|
||||
});
|
||||
|
||||
test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is false ', () => {
|
||||
test('it sorts the Last Modified column in descending order when showExtendedColumns is false ', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -357,7 +368,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -376,6 +387,7 @@ describe('TimelinesTable', () => {
|
|||
test('it displays the expected message when no search results are found', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -387,7 +399,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={[]}
|
||||
showExtendedColumnsAndActions={false}
|
||||
showExtendedColumns={false}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={0}
|
||||
|
@ -408,6 +420,7 @@ describe('TimelinesTable', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -419,7 +432,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -446,6 +459,7 @@ describe('TimelinesTable', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -457,7 +471,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -479,6 +493,7 @@ describe('TimelinesTable', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={true}
|
||||
|
@ -490,7 +505,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
@ -510,6 +525,7 @@ describe('TimelinesTable', () => {
|
|||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<TimelinesTable
|
||||
actionTimelineToShow={['delete', 'duplicate', 'selectable']}
|
||||
deleteTimelines={jest.fn()}
|
||||
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
loading={false}
|
||||
|
@ -521,7 +537,7 @@ describe('TimelinesTable', () => {
|
|||
pageIndex={0}
|
||||
pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
|
||||
searchResults={mockResults}
|
||||
showExtendedColumnsAndActions={true}
|
||||
showExtendedColumns={true}
|
||||
sortDirection={DEFAULT_SORT_DIRECTION}
|
||||
sortField={DEFAULT_SORT_FIELD}
|
||||
totalSearchResultsCount={mockResults.length}
|
||||
|
|
|
@ -10,6 +10,7 @@ import styled from 'styled-components';
|
|||
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
ActionTimelineToShow,
|
||||
DeleteTimelines,
|
||||
OnOpenTimeline,
|
||||
OnSelectionChange,
|
||||
|
@ -36,8 +37,8 @@ const BasicTable = styled(EuiBasicTable)`
|
|||
`;
|
||||
BasicTable.displayName = 'BasicTable';
|
||||
|
||||
const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) =>
|
||||
showExtendedColumnsAndActions ? [...getExtendedColumns()] : [];
|
||||
const getExtendedColumnsIfEnabled = (showExtendedColumns: boolean) =>
|
||||
showExtendedColumns ? [...getExtendedColumns()] : [];
|
||||
|
||||
/**
|
||||
* Returns the column definitions (passed as the `columns` prop to
|
||||
|
@ -46,34 +47,36 @@ const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) =>
|
|||
* `Timelines` page
|
||||
*/
|
||||
const getTimelinesTableColumns = ({
|
||||
actionTimelineToShow,
|
||||
deleteTimelines,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
onOpenTimeline,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumnsAndActions,
|
||||
showExtendedColumns,
|
||||
}: {
|
||||
actionTimelineToShow: ActionTimelineToShow[];
|
||||
deleteTimelines?: DeleteTimelines;
|
||||
itemIdToExpandedNotesRowMap: Record<string, JSX.Element>;
|
||||
onOpenTimeline: OnOpenTimeline;
|
||||
onToggleShowNotes: OnToggleShowNotes;
|
||||
showExtendedColumnsAndActions: boolean;
|
||||
showExtendedColumns: boolean;
|
||||
}) => [
|
||||
...getCommonColumns({
|
||||
itemIdToExpandedNotesRowMap,
|
||||
onOpenTimeline,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumnsAndActions,
|
||||
}),
|
||||
...getExtendedColumnsIfEnabled(showExtendedColumnsAndActions),
|
||||
...getExtendedColumnsIfEnabled(showExtendedColumns),
|
||||
...getIconHeaderColumns(),
|
||||
...getActionsColumns({
|
||||
deleteTimelines,
|
||||
onOpenTimeline,
|
||||
showDeleteAction: showExtendedColumnsAndActions,
|
||||
actionTimelineToShow,
|
||||
}),
|
||||
];
|
||||
|
||||
export interface TimelinesTableProps {
|
||||
actionTimelineToShow: ActionTimelineToShow[];
|
||||
deleteTimelines?: DeleteTimelines;
|
||||
defaultPageSize: number;
|
||||
loading: boolean;
|
||||
|
@ -85,7 +88,7 @@ export interface TimelinesTableProps {
|
|||
pageIndex: number;
|
||||
pageSize: number;
|
||||
searchResults: OpenTimelineResult[];
|
||||
showExtendedColumnsAndActions: boolean;
|
||||
showExtendedColumns: boolean;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
sortField: string;
|
||||
totalSearchResultsCount: number;
|
||||
|
@ -97,6 +100,7 @@ export interface TimelinesTableProps {
|
|||
*/
|
||||
export const TimelinesTable = React.memo<TimelinesTableProps>(
|
||||
({
|
||||
actionTimelineToShow,
|
||||
deleteTimelines,
|
||||
defaultPageSize,
|
||||
loading: isLoading,
|
||||
|
@ -108,13 +112,13 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
|
|||
pageIndex,
|
||||
pageSize,
|
||||
searchResults,
|
||||
showExtendedColumnsAndActions,
|
||||
showExtendedColumns,
|
||||
sortField,
|
||||
sortDirection,
|
||||
totalSearchResultsCount,
|
||||
}) => {
|
||||
const pagination = {
|
||||
hidePerPageOptions: !showExtendedColumnsAndActions,
|
||||
hidePerPageOptions: !showExtendedColumns,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [
|
||||
|
@ -142,16 +146,17 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
|
|||
return (
|
||||
<BasicTable
|
||||
columns={getTimelinesTableColumns({
|
||||
actionTimelineToShow,
|
||||
deleteTimelines,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
onOpenTimeline,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumnsAndActions,
|
||||
showExtendedColumns,
|
||||
})}
|
||||
compressed
|
||||
data-test-subj="timelines-table"
|
||||
isExpandable={true}
|
||||
isSelectable={showExtendedColumnsAndActions}
|
||||
isSelectable={actionTimelineToShow.includes('selectable')}
|
||||
itemId="savedObjectId"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedNotesRowMap}
|
||||
items={searchResults}
|
||||
|
@ -159,7 +164,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
|
|||
noItemsMessage={i18n.ZERO_TIMELINES_MATCH}
|
||||
onChange={onTableChange}
|
||||
pagination={pagination}
|
||||
selection={showExtendedColumnsAndActions ? selection : undefined}
|
||||
selection={actionTimelineToShow.includes('selectable') ? selection : undefined}
|
||||
sorting={sorting}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -95,6 +95,8 @@ export interface OnTableChangeParams {
|
|||
/** Invoked by the EUI table implementation when the user interacts with the table */
|
||||
export type OnTableChange = (tableChange: OnTableChangeParams) => void;
|
||||
|
||||
export type ActionTimelineToShow = 'duplicate' | 'delete' | 'selectable';
|
||||
|
||||
export interface OpenTimelineProps {
|
||||
/** Invoked when the user clicks the delete (trash) icon on an individual timeline */
|
||||
deleteTimelines?: DeleteTimelines;
|
||||
|
@ -140,6 +142,8 @@ export interface OpenTimelineProps {
|
|||
title: string;
|
||||
/** The total (server-side) count of the search results */
|
||||
totalSearchResultsCount: number;
|
||||
/** Hide action on timeline if needed it */
|
||||
hideActions?: ActionTimelineToShow[];
|
||||
}
|
||||
|
||||
export interface UpdateTimeline {
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiHighlight,
|
||||
EuiInputPopover,
|
||||
EuiSuperSelect,
|
||||
EuiSelectable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiTextColor,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { ListProps } from 'react-virtualized';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { AllTimelinesQuery } from '../../../containers/timeline/all';
|
||||
import { getEmptyTagValue } from '../../empty_value';
|
||||
import { isUntitled } from '../../../components/open_timeline/helpers';
|
||||
import * as i18nTimeline from '../../../components/open_timeline/translations';
|
||||
import { SortFieldTimeline, Direction } from '../../../graphql/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle`
|
||||
.euiPopover__panel.euiPopover__panel-isOpen.timeline-search-super-select-popover__popoverPanel {
|
||||
visibility: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const MyEuiHighlight = styled(EuiHighlight)<{ selected: boolean }>`
|
||||
padding-left: ${({ selected }) => (selected ? '3px' : '0px')};
|
||||
`;
|
||||
|
||||
const MyEuiTextColor = styled(EuiTextColor)<{ selected: boolean }>`
|
||||
padding-left: ${({ selected }) => (selected ? '20px' : '0px')};
|
||||
`;
|
||||
|
||||
interface SearchTimelineSuperSelectProps {
|
||||
isDisabled: boolean;
|
||||
timelineId: string | null;
|
||||
timelineTitle: string | null;
|
||||
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
|
||||
}
|
||||
|
||||
const basicSuperSelectOptions = [
|
||||
{
|
||||
value: '-1',
|
||||
inputDisplay: i18n.DEFAULT_TIMELINE_TITLE,
|
||||
},
|
||||
];
|
||||
|
||||
const getBasicSelectableOptions = (timelineId: string) => [
|
||||
{
|
||||
description: i18n.DEFAULT_TIMELINE_DESCRIPTION,
|
||||
label: i18n.DEFAULT_TIMELINE_TITLE,
|
||||
id: null,
|
||||
title: i18n.DEFAULT_TIMELINE_TITLE,
|
||||
checked: timelineId === '-1' ? 'on' : undefined,
|
||||
} as Option,
|
||||
];
|
||||
|
||||
const ORIGINAL_PAGE_SIZE = 50;
|
||||
const POPOVER_HEIGHT = 260;
|
||||
const TIMELINE_ITEM_HEIGHT = 50;
|
||||
const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProps> = ({
|
||||
isDisabled,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
onTimelineChange,
|
||||
}) => {
|
||||
const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE);
|
||||
const [heightTrigger, setHeightTrigger] = useState(0);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [searchTimelineValue, setSearchTimelineValue] = useState('');
|
||||
const [onlyFavorites, setOnlyFavorites] = useState(false);
|
||||
|
||||
const onSearchTimeline = useCallback(val => {
|
||||
setSearchTimelineValue(val);
|
||||
}, []);
|
||||
|
||||
const handleClosePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleOpenPopover = useCallback(() => {
|
||||
setIsPopoverOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOnToggleOnlyFavorites = useCallback(() => {
|
||||
setOnlyFavorites(!onlyFavorites);
|
||||
}, [onlyFavorites]);
|
||||
|
||||
const renderTimelineOption = useCallback((option, searchValue) => {
|
||||
return (
|
||||
<>
|
||||
{option.checked === 'on' && <EuiIcon type="check" color="primary" />}
|
||||
<MyEuiHighlight search={searchValue} selected={option.checked === 'on'}>
|
||||
{isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title}
|
||||
</MyEuiHighlight>
|
||||
<br />
|
||||
<MyEuiTextColor color="subdued" component="span" selected={option.checked === 'on'}>
|
||||
<small>
|
||||
{option.description != null && option.description.trim().length > 0
|
||||
? option.description
|
||||
: getEmptyTagValue()}
|
||||
</small>
|
||||
</MyEuiTextColor>
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTimelineChange = useCallback(options => {
|
||||
const selectedTimeline = options.filter(
|
||||
(option: { checked: string }) => option.checked === 'on'
|
||||
);
|
||||
if (selectedTimeline != null && selectedTimeline.length > 0 && onTimelineChange != null) {
|
||||
onTimelineChange(
|
||||
isEmpty(selectedTimeline[0].title)
|
||||
? i18nTimeline.UNTITLED_TIMELINE
|
||||
: selectedTimeline[0].title,
|
||||
selectedTimeline[0].id
|
||||
);
|
||||
}
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleOnScroll = useCallback(
|
||||
(
|
||||
totalTimelines: number,
|
||||
totalCount: number,
|
||||
{
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
}: {
|
||||
clientHeight: number;
|
||||
scrollHeight: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
) => {
|
||||
if (totalTimelines < totalCount) {
|
||||
const clientHeightTrigger = clientHeight * 1.2;
|
||||
if (
|
||||
scrollTop > 10 &&
|
||||
scrollHeight - scrollTop < clientHeightTrigger &&
|
||||
scrollHeight > heightTrigger
|
||||
) {
|
||||
setHeightTrigger(scrollHeight);
|
||||
setPageSize(pageSize + ORIGINAL_PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
},
|
||||
[heightTrigger, pageSize]
|
||||
);
|
||||
|
||||
const superSelect = useMemo(
|
||||
() => (
|
||||
<EuiSuperSelect
|
||||
disabled={isDisabled}
|
||||
onFocus={handleOpenPopover}
|
||||
options={
|
||||
timelineId == null
|
||||
? basicSuperSelectOptions
|
||||
: [
|
||||
{
|
||||
value: timelineId,
|
||||
inputDisplay: timelineTitle,
|
||||
},
|
||||
]
|
||||
}
|
||||
valueOfSelected={timelineId == null ? '-1' : timelineId}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers={false}
|
||||
popoverClassName="timeline-search-super-select-popover"
|
||||
/>
|
||||
),
|
||||
[handleOpenPopover, isDisabled, timelineId, timelineTitle]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiInputPopover
|
||||
id="searchTimelinePopover"
|
||||
input={superSelect}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={handleClosePopover}
|
||||
>
|
||||
<AllTimelinesQuery
|
||||
pageInfo={{
|
||||
pageIndex: 1,
|
||||
pageSize,
|
||||
}}
|
||||
search={searchTimelineValue}
|
||||
sort={{ sortField: SortFieldTimeline.updated, sortOrder: Direction.desc }}
|
||||
onlyUserFavorite={onlyFavorites}
|
||||
>
|
||||
{({ timelines, loading, totalCount }) => (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFilterGroup>
|
||||
<EuiFilterButton
|
||||
size="xs"
|
||||
data-test-subj="only-favorites-toggle"
|
||||
hasActiveFilters={onlyFavorites}
|
||||
onClick={handleOnToggleOnlyFavorites}
|
||||
>
|
||||
{i18nTimeline.ONLY_FAVORITES}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiSelectable
|
||||
height={POPOVER_HEIGHT}
|
||||
isLoading={loading && timelines.length === 0}
|
||||
listProps={{
|
||||
rowHeight: TIMELINE_ITEM_HEIGHT,
|
||||
showIcons: false,
|
||||
virtualizedProps: ({
|
||||
onScroll: handleOnScroll.bind(null, timelines.length, totalCount),
|
||||
} as unknown) as ListProps,
|
||||
}}
|
||||
renderOption={renderTimelineOption}
|
||||
onChange={handleTimelineChange}
|
||||
searchable
|
||||
searchProps={{
|
||||
'data-test-subj': 'timeline-super-select-search-box',
|
||||
isLoading: loading,
|
||||
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
|
||||
onSearch: onSearchTimeline,
|
||||
incremental: false,
|
||||
}}
|
||||
singleSelection={true}
|
||||
options={[
|
||||
...(!onlyFavorites && searchTimelineValue === ''
|
||||
? getBasicSelectableOptions(timelineId == null ? '-1' : timelineId)
|
||||
: []),
|
||||
...timelines.map(
|
||||
(t, index) =>
|
||||
({
|
||||
description: t.description,
|
||||
label: t.title,
|
||||
id: t.savedObjectId,
|
||||
key: `${t.title}-${index}`,
|
||||
title: t.title,
|
||||
checked: t.savedObjectId === timelineId ? 'on' : undefined,
|
||||
} as Option)
|
||||
),
|
||||
]}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
{list}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</>
|
||||
)}
|
||||
</AllTimelinesQuery>
|
||||
<SearchTimelineSuperSelectGlobalStyle />
|
||||
</EuiInputPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchTimelineSuperSelect = memo(SearchTimelineSuperSelectComponent);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DEFAULT_TIMELINE_TITLE = i18n.translate('xpack.siem.timeline.defaultTimelineTitle', {
|
||||
defaultMessage: 'Default blank timeline',
|
||||
});
|
||||
|
||||
export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate(
|
||||
'xpack.siem.timeline.defaultTimelineDescription',
|
||||
{
|
||||
defaultMessage: 'Timeline offered by default when creating new timeline.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate(
|
||||
'xpack.siem.timeline.searchBoxPlaceholder',
|
||||
{
|
||||
defaultMessage: 'e.g. timeline name or description',
|
||||
}
|
||||
);
|
|
@ -70,8 +70,8 @@ export const RuleSchema = t.intersection([
|
|||
risk_score: t.number,
|
||||
rule_id: t.string,
|
||||
severity: t.string,
|
||||
type: t.string,
|
||||
tags: t.array(t.string),
|
||||
type: t.string,
|
||||
to: t.string,
|
||||
threats: t.array(t.unknown),
|
||||
updated_at: t.string,
|
||||
|
@ -79,6 +79,8 @@ export const RuleSchema = t.intersection([
|
|||
}),
|
||||
t.partial({
|
||||
saved_id: t.string,
|
||||
timeline_id: t.string,
|
||||
timeline_title: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -56,13 +56,13 @@ export const DetectionEngineContainer = React.memo<Props>(() => {
|
|||
<Route exact path={`${detectionEnginePath}/rules`}>
|
||||
<RulesComponent />
|
||||
</Route>
|
||||
<Route path={`${detectionEnginePath}/rules/create`}>
|
||||
<Route exact path={`${detectionEnginePath}/rules/create`}>
|
||||
<CreateRuleComponent />
|
||||
</Route>
|
||||
<Route exact path={`${detectionEnginePath}/rules/:ruleId`}>
|
||||
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId`}>
|
||||
<RuleDetailsComponent signalsIndex={signalIndexName} />
|
||||
</Route>
|
||||
<Route path={`${detectionEnginePath}/rules/:ruleId/edit`}>
|
||||
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId/edit`}>
|
||||
<EditRuleComponent />
|
||||
</Route>
|
||||
</>
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { Action } from './reducer';
|
||||
|
||||
export const editRuleAction = (rule: Rule, history: H.History) => {
|
||||
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/${rule.id}/edit`);
|
||||
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`);
|
||||
};
|
||||
|
||||
export const runRuleAction = () => {};
|
||||
|
|
|
@ -13,7 +13,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[]
|
|||
id: rule.id,
|
||||
rule_id: rule.rule_id,
|
||||
rule: {
|
||||
href: `#/detection-engine/rules/${encodeURIComponent(rule.id)}`,
|
||||
href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`,
|
||||
name: rule.name,
|
||||
status: 'Status Placeholder',
|
||||
},
|
||||
|
|
|
@ -4,9 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as RuleI18n from '../../translations';
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
|
||||
|
@ -17,11 +26,26 @@ interface AddItemProps {
|
|||
dataTestSubj: string;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
validate?: (args: unknown) => boolean;
|
||||
}
|
||||
|
||||
export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => {
|
||||
const MyEuiFormRow = styled(EuiFormRow)`
|
||||
.euiFormRow__labelWrapper {
|
||||
.euiText {
|
||||
padding-right: 32px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AddItem = ({
|
||||
addText,
|
||||
dataTestSubj,
|
||||
field,
|
||||
idAria,
|
||||
isDisabled,
|
||||
validate,
|
||||
}: AddItemProps) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
// const [items, setItems] = useState(['']);
|
||||
const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1);
|
||||
|
||||
const inputsRef = useRef<HTMLInputElement[]>([]);
|
||||
|
@ -104,7 +128,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
|
|||
|
||||
const values = field.value as string[];
|
||||
return (
|
||||
<EuiFormRow
|
||||
<MyEuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
error={errorMessage}
|
||||
|
@ -124,11 +148,15 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
|
|||
inputsRef.current[index] == null
|
||||
? { value: item }
|
||||
: {}),
|
||||
isInvalid: validate == null ? false : validate(item),
|
||||
};
|
||||
return (
|
||||
<div key={index}>
|
||||
<EuiFieldText
|
||||
append={
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow>
|
||||
<EuiFieldText onChange={e => updateItem(e, index)} fullWidth {...euiFieldProps} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
|
@ -136,21 +164,18 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad
|
|||
onClick={() => removeItem(index)}
|
||||
aria-label={RuleI18n.DELETE}
|
||||
/>
|
||||
}
|
||||
onChange={e => updateItem(e, index)}
|
||||
compressed
|
||||
fullWidth
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{values.length - 1 !== index && <EuiSpacer size="s" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled}>
|
||||
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled} iconType="plusInCircle">
|
||||
{addText}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</MyEuiFormRow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiListGroup,
|
||||
} from '@elastic/eui';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
|
||||
import { FilterLabel } from './filter_label';
|
||||
import * as i18n from './translations';
|
||||
import { BuildQueryBarDescription, BuildThreatsDescription, ListItems } from './types';
|
||||
|
||||
const EuiBadgeWrap = styled(EuiBadge)`
|
||||
.euiBadge__text {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const buildQueryBarDescription = ({
|
||||
field,
|
||||
filters,
|
||||
filterManager,
|
||||
query,
|
||||
savedId,
|
||||
indexPatterns,
|
||||
}: BuildQueryBarDescription): ListItems[] => {
|
||||
let items: ListItems[] = [];
|
||||
if (!isEmpty(filters)) {
|
||||
filterManager.setFilters(filters);
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.FILTERS_LABEL} </>,
|
||||
description: (
|
||||
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
|
||||
{filterManager.getFilters().map((filter, index) => (
|
||||
<EuiFlexItem grow={false} key={`${field}-filter-${index}`}>
|
||||
<EuiBadgeWrap color="hollow">
|
||||
{indexPatterns != null ? (
|
||||
<FilterLabel
|
||||
filter={filter}
|
||||
valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])}
|
||||
/>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
)}
|
||||
</EuiBadgeWrap>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(query.query)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.QUERY_LABEL} </>,
|
||||
description: <>{query.query} </>,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(savedId)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.SAVED_ID_LABEL} </>,
|
||||
description: <>{savedId} </>,
|
||||
},
|
||||
];
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
const ThreatsEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
.euiFlexItem {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MyEuiListGroup = styled(EuiListGroup)`
|
||||
padding: 0px;
|
||||
.euiListGroupItem__button {
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const buildThreatsDescription = ({
|
||||
label,
|
||||
threats,
|
||||
}: BuildThreatsDescription): ListItems[] => {
|
||||
if (threats.length > 0) {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<ThreatsEuiFlexGroup direction="column">
|
||||
{threats.map((threat, index) => {
|
||||
const tactic = tacticsOptions.find(t => t.name === threat.tactic.name);
|
||||
return (
|
||||
<EuiFlexItem key={`${threat.tactic.name}-${index}`}>
|
||||
<EuiText grow={false} size="s">
|
||||
<h5>
|
||||
<EuiLink href={threat.tactic.reference} target="_blank">
|
||||
{tactic != null ? tactic.text : ''}
|
||||
</EuiLink>
|
||||
</h5>
|
||||
<MyEuiListGroup
|
||||
flush={false}
|
||||
bordered={false}
|
||||
listItems={threat.techniques.map(technique => {
|
||||
const myTechnique = techniquesOptions.find(t => t.name === technique.name);
|
||||
return {
|
||||
label: myTechnique != null ? myTechnique.label : '',
|
||||
href: technique.reference,
|
||||
target: '_blank',
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</ThreatsEuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const buildStringArrayDescription = (
|
||||
label: string,
|
||||
field: string,
|
||||
values: string[]
|
||||
): ListItems[] => {
|
||||
if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" wrap>
|
||||
{values.map((val: string) =>
|
||||
isEmpty(val) ? null : (
|
||||
<EuiFlexItem grow={false} key={`${field}-${val}`}>
|
||||
<EuiBadgeWrap color="hollow">{val}</EuiBadgeWrap>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const buildSeverityDescription = (label: string, value: string): ListItems[] => {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<EuiHealth
|
||||
color={
|
||||
value === 'low'
|
||||
? euiLightVars.euiColorVis0
|
||||
: value === 'medium'
|
||||
? euiLightVars.euiColorVis5
|
||||
: value === 'high'
|
||||
? euiLightVars.euiColorVis7
|
||||
: euiLightVars.euiColorVis9
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => {
|
||||
if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<EuiListGroup
|
||||
flush={true}
|
||||
bordered={false}
|
||||
listItems={values.map((val: string) => ({
|
||||
label: val,
|
||||
href: val,
|
||||
iconType: 'link',
|
||||
size: 'xs',
|
||||
target: '_blank',
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
|
@ -4,19 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiDescriptionList,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTextArea,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiListGroup,
|
||||
} from '@elastic/eui';
|
||||
import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui';
|
||||
import { isEmpty, chunk, get, pick } from 'lodash/fp';
|
||||
import React, { memo, ReactNode, useState } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
|
@ -25,13 +15,19 @@ import {
|
|||
FilterManager,
|
||||
Query,
|
||||
} from '../../../../../../../../../../src/plugins/data/public';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations';
|
||||
import { useKibana } from '../../../../../lib/kibana';
|
||||
import { FilterLabel } from './filter_label';
|
||||
import { FormSchema } from '../shared_imports';
|
||||
import * as I18n from './translations';
|
||||
|
||||
import { IMitreEnterpriseAttack } from '../../types';
|
||||
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
import { FieldValueTimeline } from '../pick_timeline';
|
||||
import { FormSchema } from '../shared_imports';
|
||||
import { ListItems } from './types';
|
||||
import {
|
||||
buildQueryBarDescription,
|
||||
buildSeverityDescription,
|
||||
buildStringArrayDescription,
|
||||
buildThreatsDescription,
|
||||
buildUrlsDescription,
|
||||
} from './helpers';
|
||||
|
||||
interface StepRuleDescriptionProps {
|
||||
direction?: 'row' | 'column';
|
||||
|
@ -40,29 +36,10 @@ interface StepRuleDescriptionProps {
|
|||
schema: FormSchema;
|
||||
}
|
||||
|
||||
const EuiBadgeWrap = styled(EuiBadge)`
|
||||
.euiBadge__text {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>`
|
||||
${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')};
|
||||
`;
|
||||
|
||||
const MyEuiListGroup = styled(EuiListGroup)`
|
||||
padding: 0px;
|
||||
.euiListGroupItem__button {
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ThreatsEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
.euiFlexItem {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MyEuiTextArea = styled(EuiTextArea)`
|
||||
max-width: 100%;
|
||||
height: 80px;
|
||||
|
@ -87,9 +64,9 @@ const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
|
|||
);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction={direction} justifyContent="spaceAround">
|
||||
{chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => (
|
||||
{chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => (
|
||||
<EuiFlexItemWidth direction={direction} key={`description-step-rule-${index}`} grow={false}>
|
||||
<EuiDescriptionList listItems={chunckListItems} compressed />
|
||||
<EuiDescriptionList listItems={chunkListItems} />
|
||||
</EuiFlexItemWidth>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
@ -98,11 +75,6 @@ const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
|
|||
|
||||
export const StepRuleDescription = memo(StepRuleDescriptionComponent);
|
||||
|
||||
interface ListItems {
|
||||
title: NonNullable<ReactNode>;
|
||||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
const buildListItems = (
|
||||
data: unknown,
|
||||
schema: FormSchema,
|
||||
|
@ -130,103 +102,23 @@ const getDescriptionItem = (
|
|||
filterManager: FilterManager,
|
||||
indexPatterns?: IIndexPattern
|
||||
): ListItems[] => {
|
||||
if (field === 'useIndicesConfig') {
|
||||
return [];
|
||||
} else if (field === 'queryBar') {
|
||||
if (field === 'queryBar') {
|
||||
const filters = get('queryBar.filters', value) as esFilters.Filter[];
|
||||
const query = get('queryBar.query', value) as Query;
|
||||
const savedId = get('queryBar.saved_id', value);
|
||||
let items: ListItems[] = [];
|
||||
if (!isEmpty(filters)) {
|
||||
filterManager.setFilters(filters);
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{I18n.FILTERS_LABEL}</>,
|
||||
description: (
|
||||
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
|
||||
{filterManager.getFilters().map((filter, index) => (
|
||||
<EuiFlexItem grow={false} key={`${field}-filter-${index}`}>
|
||||
<EuiBadgeWrap color="hollow">
|
||||
{indexPatterns != null ? (
|
||||
<FilterLabel
|
||||
filter={filter}
|
||||
valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])}
|
||||
/>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
)}
|
||||
</EuiBadgeWrap>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(query.query)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{I18n.QUERY_LABEL}</>,
|
||||
description: <>{query.query}</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(savedId)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{I18n.SAVED_ID_LABEL}</>,
|
||||
description: <>{savedId}</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
return items;
|
||||
return buildQueryBarDescription({
|
||||
field,
|
||||
filters,
|
||||
filterManager,
|
||||
query,
|
||||
savedId,
|
||||
indexPatterns,
|
||||
});
|
||||
} else if (field === 'threats') {
|
||||
const threats: IMitreEnterpriseAttack[] = get(field, value).filter(
|
||||
(threat: IMitreEnterpriseAttack) => threat.tactic.name !== 'none'
|
||||
);
|
||||
if (threats.length > 0) {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<ThreatsEuiFlexGroup direction="column">
|
||||
{threats.map((threat, index) => {
|
||||
const tactic = tacticsOptions.find(t => t.name === threat.tactic.name);
|
||||
return (
|
||||
<EuiFlexItem key={`${threat.tactic.name}-${index}`}>
|
||||
<EuiText grow={false} size="s">
|
||||
<h5>
|
||||
<EuiLink href={threat.tactic.reference} target="_blank">
|
||||
{tactic != null ? tactic.text : ''}
|
||||
</EuiLink>
|
||||
</h5>
|
||||
<MyEuiListGroup
|
||||
flush={false}
|
||||
bordered={false}
|
||||
listItems={threat.techniques.map(technique => {
|
||||
const myTechnique = techniquesOptions.find(
|
||||
t => t.name === technique.name
|
||||
);
|
||||
return {
|
||||
label: myTechnique != null ? myTechnique.label : '',
|
||||
href: technique.reference,
|
||||
target: '_blank',
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</ThreatsEuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
return buildThreatsDescription({ label, threats });
|
||||
} else if (field === 'description') {
|
||||
return [
|
||||
{
|
||||
|
@ -234,27 +126,23 @@ const getDescriptionItem = (
|
|||
description: <MyEuiTextArea value={get(field, value)} readOnly={true} />,
|
||||
},
|
||||
];
|
||||
} else if (field === 'references') {
|
||||
const urls: string[] = get(field, value);
|
||||
return buildUrlsDescription(label, urls);
|
||||
} else if (Array.isArray(get(field, value))) {
|
||||
const values: string[] = get(field, value);
|
||||
if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) {
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" wrap>
|
||||
{values.map((val: string) =>
|
||||
isEmpty(val) ? null : (
|
||||
<EuiFlexItem grow={false} key={`${field}-${val}`}>
|
||||
<EuiBadgeWrap color="hollow">{val}</EuiBadgeWrap>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
return buildStringArrayDescription(label, field, values);
|
||||
} else if (field === 'severity') {
|
||||
const val: string = get(field, value);
|
||||
return buildSeverityDescription(label, val);
|
||||
} else if (field === 'timeline') {
|
||||
const timeline = get(field, value) as FieldValueTimeline;
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: timeline.title ?? DEFAULT_TIMELINE_TITLE,
|
||||
},
|
||||
];
|
||||
}
|
||||
const description: string = get(field, value);
|
||||
if (!isEmpty(description)) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
IIndexPattern,
|
||||
esFilters,
|
||||
FilterManager,
|
||||
Query,
|
||||
} from '../../../../../../../../../../src/plugins/data/public';
|
||||
import { IMitreEnterpriseAttack } from '../../types';
|
||||
|
||||
export interface ListItems {
|
||||
title: NonNullable<ReactNode>;
|
||||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
export interface BuildQueryBarDescription {
|
||||
field: string;
|
||||
filters: esFilters.Filter[];
|
||||
filterManager: FilterManager;
|
||||
query: Query;
|
||||
savedId: string;
|
||||
indexPatterns?: IIndexPattern;
|
||||
}
|
||||
|
||||
export interface BuildThreatsDescription {
|
||||
label: string;
|
||||
threats: IMitreEnterpriseAttack[];
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import { IMitreAttack } from '../../types';
|
||||
|
||||
export const isMitreAttackInvalid = (
|
||||
tacticName: string | null | undefined,
|
||||
techniques: IMitreAttack[] | null | undefined
|
||||
) => {
|
||||
if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(techniques))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -8,27 +8,30 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSuperSelect,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiComboBox,
|
||||
EuiFormControlLayout,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty, kebabCase, camelCase } from 'lodash/fp';
|
||||
import React, { ChangeEvent, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques';
|
||||
import * as RuleI18n from '../../translations';
|
||||
import * as Rulei18n from '../../translations';
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
|
||||
import * as I18n from './translations';
|
||||
import { threatsDefault } from '../step_about_rule/default_value';
|
||||
import { IMitreEnterpriseAttack } from '../../types';
|
||||
import { isMitreAttackInvalid } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MyEuiFormControlLayout = styled(EuiFormControlLayout)`
|
||||
&.euiFormControlLayout--compressed {
|
||||
height: fit-content !important;
|
||||
}
|
||||
const MitreContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
`;
|
||||
const MyEuiSuperSelect = styled(EuiSuperSelect)`
|
||||
width: 280px;
|
||||
`;
|
||||
interface AddItemProps {
|
||||
field: FieldHook;
|
||||
|
@ -43,7 +46,12 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
|
|||
const removeItem = useCallback(
|
||||
(index: number) => {
|
||||
const values = field.value as string[];
|
||||
field.setValue([...values.slice(0, index), ...values.slice(index + 1)]);
|
||||
const newValues = [...values.slice(0, index), ...values.slice(index + 1)];
|
||||
if (isEmpty(newValues)) {
|
||||
field.setValue(threatsDefault);
|
||||
} else {
|
||||
field.setValue(newValues);
|
||||
}
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
@ -61,9 +69,9 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
|
|||
}, [field]);
|
||||
|
||||
const updateTactic = useCallback(
|
||||
(index: number, event: ChangeEvent<HTMLSelectElement>) => {
|
||||
(index: number, value: string) => {
|
||||
const values = field.value as IMitreEnterpriseAttack[];
|
||||
const { id, reference, name } = tacticsOptions.find(t => t.value === event.target.value) || {
|
||||
const { id, reference, name } = tacticsOptions.find(t => t.value === value) || {
|
||||
id: '',
|
||||
name: '',
|
||||
reference: '',
|
||||
|
@ -97,75 +105,104 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI
|
|||
|
||||
const values = field.value as IMitreEnterpriseAttack[];
|
||||
|
||||
const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => (
|
||||
<MyEuiSuperSelect
|
||||
id="selectDocExample"
|
||||
options={[
|
||||
...(tacticName === 'none'
|
||||
? [
|
||||
{
|
||||
inputDisplay: <>{i18n.TACTIC_PLACEHOLDER}</>,
|
||||
value: 'none',
|
||||
disabled,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...tacticsOptions.map(t => ({
|
||||
inputDisplay: <>{t.text}</>,
|
||||
value: t.value,
|
||||
disabled,
|
||||
})),
|
||||
]}
|
||||
aria-label=""
|
||||
onChange={updateTactic.bind(null, index)}
|
||||
fullWidth={false}
|
||||
valueOfSelected={camelCase(tacticName)}
|
||||
/>
|
||||
);
|
||||
|
||||
const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => {
|
||||
const invalid = isMitreAttackInvalid(item.tactic.name, item.techniques);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.TECHNIQUES_PLACEHOLDER}
|
||||
options={techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name)))}
|
||||
selectedOptions={item.techniques}
|
||||
onChange={updateTechniques.bind(null, index)}
|
||||
isDisabled={disabled}
|
||||
fullWidth={true}
|
||||
isInvalid={invalid}
|
||||
/>
|
||||
{invalid && (
|
||||
<EuiText color="danger" size="xs">
|
||||
<p>{errorMessage}</p>
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
isDisabled={disabled}
|
||||
onClick={() => removeItem(index)}
|
||||
aria-label={Rulei18n.DELETE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
<>
|
||||
{values.map((item, index) => {
|
||||
const euiSelectFieldProps = {
|
||||
disabled: isDisabled,
|
||||
};
|
||||
return (
|
||||
<div key={index}>
|
||||
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
id="selectDocExample"
|
||||
options={[
|
||||
...(item.tactic.name === 'none'
|
||||
? [{ text: I18n.TACTIC_PLACEHOLDER, value: 'none' }]
|
||||
: []),
|
||||
...tacticsOptions.map(t => ({ text: t.text, value: t.value })),
|
||||
]}
|
||||
aria-label=""
|
||||
onChange={updateTactic.bind(null, index)}
|
||||
prepend={I18n.TACTIC}
|
||||
compressed
|
||||
fullWidth={false}
|
||||
value={camelCase(item.tactic.name)}
|
||||
{...euiSelectFieldProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<MyEuiFormControlLayout compressed fullWidth prepend={I18n.TECHNIQUES}>
|
||||
<EuiComboBox
|
||||
compressed
|
||||
placeholder={I18n.TECHNIQUES_PLACEHOLDER}
|
||||
options={techniquesOptions.filter(t =>
|
||||
t.tactics.includes(kebabCase(item.tactic.name))
|
||||
)}
|
||||
selectedOptions={item.techniques}
|
||||
onChange={updateTechniques.bind(null, index)}
|
||||
isDisabled={isDisabled}
|
||||
fullWidth={true}
|
||||
/>
|
||||
</MyEuiFormControlLayout>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => removeItem(index)}
|
||||
aria-label={RuleI18n.DELETE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{values.length - 1 !== index && <EuiSpacer size="s" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled}>
|
||||
{I18n.ADD_MITRE_ATTACK}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
<MitreContainer>
|
||||
{values.map((item, index) => (
|
||||
<div key={index}>
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween" alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
{index === 0 ? (
|
||||
<EuiFormRow
|
||||
label={`${field.label} ${i18n.TACTIC}`}
|
||||
labelAppend={field.labelAppend}
|
||||
describedByIds={idAria ? [`${idAria} ${i18n.TACTIC}`] : undefined}
|
||||
>
|
||||
<>{getSelectTactic(item.tactic.name, index, isDisabled)}</>
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
getSelectTactic(item.tactic.name, index, isDisabled)
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
{index === 0 ? (
|
||||
<EuiFormRow
|
||||
label={`${field.label} ${i18n.TECHNIQUE}`}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
describedByIds={idAria ? [`${idAria} ${i18n.TECHNIQUE}`] : undefined}
|
||||
>
|
||||
<>{getSelectTechniques(item, index, isDisabled)}</>
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
getSelectTechniques(item, index, isDisabled)
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{values.length - 1 !== index && <EuiSpacer size="s" />}
|
||||
</div>
|
||||
))}
|
||||
<EuiButtonEmpty size="xs" onClick={addItem} isDisabled={isDisabled} iconType="plusInCircle">
|
||||
{i18n.ADD_MITRE_ATTACK}
|
||||
</EuiButtonEmpty>
|
||||
</MitreContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TACTIC = i18n.translate('xpack.siem.detectionEngine.mitreAttack.tacticsDescription', {
|
||||
defaultMessage: 'Tactic',
|
||||
defaultMessage: 'tactic',
|
||||
});
|
||||
|
||||
export const TECHNIQUES = i18n.translate(
|
||||
export const TECHNIQUE = i18n.translate(
|
||||
'xpack.siem.detectionEngine.mitreAttack.techniquesDescription',
|
||||
{
|
||||
defaultMessage: 'Techniques',
|
||||
defaultMessage: 'technique',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select';
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
|
||||
|
||||
export interface FieldValueTimeline {
|
||||
id: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
interface QueryBarDefineRuleProps {
|
||||
dataTestSubj: string;
|
||||
field: FieldHook;
|
||||
idAria: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export const PickTimeline = ({
|
||||
dataTestSubj,
|
||||
field,
|
||||
idAria,
|
||||
isDisabled = false,
|
||||
}: QueryBarDefineRuleProps) => {
|
||||
const [timelineId, setTimelineId] = useState<string | null>(null);
|
||||
const [timelineTitle, setTimelineTitle] = useState<string | null>(null);
|
||||
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
useEffect(() => {
|
||||
const { id, title } = field.value as FieldValueTimeline;
|
||||
if (timelineTitle !== title && timelineId !== id) {
|
||||
setTimelineId(id);
|
||||
setTimelineTitle(title);
|
||||
}
|
||||
}, [field.value]);
|
||||
|
||||
const handleOnTimelineChange = useCallback(
|
||||
(title: string, id: string | null) => {
|
||||
if (id === null) {
|
||||
field.setValue({ id, title: null });
|
||||
} else if (timelineTitle !== title && timelineId !== id) {
|
||||
field.setValue({ id, title });
|
||||
}
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
<SearchTimelineSuperSelect
|
||||
isDisabled={isDisabled}
|
||||
timelineId={timelineId}
|
||||
timelineTitle={timelineTitle}
|
||||
onTimelineChange={handleOnTimelineChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiFormRow, EuiMutationObserver } from '@elastic/eui';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -19,11 +19,18 @@ import {
|
|||
SavedQueryTimeFilter,
|
||||
} from '../../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { BrowserFields } from '../../../../../containers/source';
|
||||
import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal';
|
||||
import { ActionTimelineToShow } from '../../../../../components/open_timeline/types';
|
||||
import { QueryBar } from '../../../../../components/query_bar';
|
||||
import { buildGlobalQuery } from '../../../../../components/timeline/helpers';
|
||||
import { getDataProviderFilter } from '../../../../../components/timeline/query_bar';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury';
|
||||
import { useKibana } from '../../../../../lib/kibana';
|
||||
import { TimelineModel } from '../../../../../store/timeline/model';
|
||||
import { useSavedQueryServices } from '../../../../../utils/saved_query_services';
|
||||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface FieldValueQueryBar {
|
||||
filters: esFilters.Filter[];
|
||||
|
@ -31,11 +38,14 @@ export interface FieldValueQueryBar {
|
|||
saved_id: string | null;
|
||||
}
|
||||
interface QueryBarDefineRuleProps {
|
||||
browserFields: BrowserFields;
|
||||
dataTestSubj: string;
|
||||
field: FieldHook;
|
||||
idAria: string;
|
||||
isLoading: boolean;
|
||||
indexPattern: IIndexPattern;
|
||||
onCloseTimelineSearch: () => void;
|
||||
openTimelineSearch: boolean;
|
||||
resizeParentContainer?: (height: number) => void;
|
||||
}
|
||||
|
||||
|
@ -56,14 +66,18 @@ const StyledEuiFormRow = styled(EuiFormRow)`
|
|||
// TODO need to add disabled in the SearchBar
|
||||
|
||||
export const QueryBarDefineRule = ({
|
||||
browserFields,
|
||||
dataTestSubj,
|
||||
field,
|
||||
idAria,
|
||||
indexPattern,
|
||||
isLoading = false,
|
||||
onCloseTimelineSearch,
|
||||
openTimelineSearch = false,
|
||||
resizeParentContainer,
|
||||
}: QueryBarDefineRuleProps) => {
|
||||
const [originalHeight, setOriginalHeight] = useState(-1);
|
||||
const [loadingTimeline, setLoadingTimeline] = useState(false);
|
||||
const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null);
|
||||
const [queryDraft, setQueryDraft] = useState<Query>({ query: '', language: 'kuery' });
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
@ -168,6 +182,38 @@ export const QueryBarDefineRule = ({
|
|||
[field.value]
|
||||
);
|
||||
|
||||
const onCloseTimelineModal = useCallback(() => {
|
||||
setLoadingTimeline(true);
|
||||
onCloseTimelineSearch();
|
||||
}, [onCloseTimelineSearch]);
|
||||
|
||||
const onOpenTimeline = useCallback(
|
||||
(timeline: TimelineModel) => {
|
||||
setLoadingTimeline(false);
|
||||
const newQuery = {
|
||||
query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '',
|
||||
language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery',
|
||||
};
|
||||
const dataProvidersDsl =
|
||||
timeline.dataProviders != null && timeline.dataProviders.length > 0
|
||||
? convertKueryToElasticSearchQuery(
|
||||
buildGlobalQuery(timeline.dataProviders, browserFields),
|
||||
indexPattern
|
||||
)
|
||||
: '';
|
||||
const newFilters = timeline.filters ?? [];
|
||||
field.setValue({
|
||||
filters:
|
||||
dataProvidersDsl !== ''
|
||||
? [...newFilters, getDataProviderFilter(dataProvidersDsl)]
|
||||
: newFilters,
|
||||
query: newQuery,
|
||||
saved_id: '',
|
||||
});
|
||||
},
|
||||
[browserFields, field, indexPattern]
|
||||
);
|
||||
|
||||
const onMutation = (event: unknown, observer: unknown) => {
|
||||
if (resizeParentContainer != null) {
|
||||
const suggestionContainer = document.getElementById('kbnTypeahead__items');
|
||||
|
@ -189,39 +235,51 @@ export const QueryBarDefineRule = ({
|
|||
}
|
||||
};
|
||||
|
||||
const actionTimelineToHide = useMemo<ActionTimelineToShow[]>(() => ['duplicate'], []);
|
||||
|
||||
return (
|
||||
<StyledEuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
<EuiMutationObserver
|
||||
observerOptions={{ subtree: true, attributes: true, childList: true }}
|
||||
onMutation={onMutation}
|
||||
<>
|
||||
<StyledEuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
{mutationRef => (
|
||||
<div ref={mutationRef}>
|
||||
<QueryBar
|
||||
indexPattern={indexPattern}
|
||||
isLoading={isLoading}
|
||||
isRefreshPaused={false}
|
||||
filterQuery={queryDraft}
|
||||
filterManager={filterManager}
|
||||
filters={filterManager.getFilters() || []}
|
||||
onChangedQuery={onChangedQuery}
|
||||
onSubmitQuery={onSubmitQuery}
|
||||
savedQuery={savedQuery}
|
||||
onSavedQuery={onSavedQuery}
|
||||
hideSavedQuery={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiMutationObserver>
|
||||
</StyledEuiFormRow>
|
||||
<EuiMutationObserver
|
||||
observerOptions={{ subtree: true, attributes: true, childList: true }}
|
||||
onMutation={onMutation}
|
||||
>
|
||||
{mutationRef => (
|
||||
<div ref={mutationRef}>
|
||||
<QueryBar
|
||||
indexPattern={indexPattern}
|
||||
isLoading={isLoading || loadingTimeline}
|
||||
isRefreshPaused={false}
|
||||
filterQuery={queryDraft}
|
||||
filterManager={filterManager}
|
||||
filters={filterManager.getFilters() || []}
|
||||
onChangedQuery={onChangedQuery}
|
||||
onSubmitQuery={onSubmitQuery}
|
||||
savedQuery={savedQuery}
|
||||
onSavedQuery={onSavedQuery}
|
||||
hideSavedQuery={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiMutationObserver>
|
||||
</StyledEuiFormRow>
|
||||
{openTimelineSearch ? (
|
||||
<OpenTimelineModal
|
||||
hideActions={actionTimelineToHide}
|
||||
modalTitle={i18n.IMPORT_TIMELINE_MODAL}
|
||||
onClose={onCloseTimelineModal}
|
||||
onOpen={onOpenTimeline}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const IMPORT_TIMELINE_MODAL = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle',
|
||||
{
|
||||
defaultMessage: 'Import query from saved timeline',
|
||||
}
|
||||
);
|
|
@ -4,9 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports';
|
||||
|
@ -32,6 +32,10 @@ const StyledEuiFormRow = styled(EuiFormRow)`
|
|||
}
|
||||
`;
|
||||
|
||||
const MyEuiSelect = styled(EuiSelect)`
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => {
|
||||
const [timeType, setTimeType] = useState('s');
|
||||
const [timeVal, setTimeVal] = useState<number>(0);
|
||||
|
@ -79,22 +83,33 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
|
|||
|
||||
// EUI missing some props
|
||||
const rest = { disabled: isDisabled };
|
||||
const label = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem grow={false} component="span">
|
||||
{field.label}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} component="span">
|
||||
{field.labelAppend}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[field.label, field.labelAppend]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledEuiFormRow
|
||||
label={field.label}
|
||||
labelAppend={field.labelAppend}
|
||||
label={label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
fullWidth={false}
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
append={
|
||||
<EuiSelect
|
||||
compressed={true}
|
||||
<MyEuiSelect
|
||||
fullWidth={false}
|
||||
options={timeTypeOptions}
|
||||
onChange={onChangeTimeType}
|
||||
|
@ -102,7 +117,6 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu
|
|||
{...rest}
|
||||
/>
|
||||
}
|
||||
compressed
|
||||
fullWidth
|
||||
min={0}
|
||||
onChange={onChangeTimeVal}
|
||||
|
|
|
@ -27,7 +27,7 @@ const RuleStatusIconStyled = styled.div`
|
|||
|
||||
const RuleStatusIconComponent: React.FC<RuleStatusIconProps> = ({ name, type }) => {
|
||||
const theme = useEuiTheme();
|
||||
const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorDarkestShade;
|
||||
const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorPrimary;
|
||||
return (
|
||||
<RuleStatusIconStyled>
|
||||
<EuiAvatar color={color} name={type === 'valid' ? '' : name} size="l" />
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as I18n from './translations';
|
||||
|
||||
export type SeverityValue = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
interface SeverityOptionItem {
|
||||
value: SeverityValue;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const severityOptions: SeverityOptionItem[] = [
|
||||
{ value: 'low', text: I18n.LOW },
|
||||
{ value: 'medium', text: I18n.MEDIUM },
|
||||
{ value: 'high', text: I18n.HIGH },
|
||||
{ value: 'critical', text: I18n.CRITICAL },
|
||||
];
|
||||
|
||||
export const defaultRiskScoreBySeverity: Record<SeverityValue, number> = {
|
||||
low: 21,
|
||||
medium: 47,
|
||||
high: 73,
|
||||
critical: 99,
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiHealth } from '@elastic/eui';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import React from 'react';
|
||||
import * as I18n from './translations';
|
||||
|
||||
export type SeverityValue = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
interface SeverityOptionItem {
|
||||
value: SeverityValue;
|
||||
inputDisplay: React.ReactElement;
|
||||
}
|
||||
|
||||
export const severityOptions: SeverityOptionItem[] = [
|
||||
{
|
||||
value: 'low',
|
||||
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis0}>{I18n.LOW}</EuiHealth>,
|
||||
},
|
||||
{
|
||||
value: 'medium',
|
||||
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis5}>{I18n.MEDIUM} </EuiHealth>,
|
||||
},
|
||||
{
|
||||
value: 'high',
|
||||
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis7}>{I18n.HIGH} </EuiHealth>,
|
||||
},
|
||||
{
|
||||
value: 'critical',
|
||||
inputDisplay: <EuiHealth color={euiLightVars.euiColorVis9}>{I18n.CRITICAL} </EuiHealth>,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultRiskScoreBySeverity: Record<SeverityValue, number> = {
|
||||
low: 21,
|
||||
medium: 47,
|
||||
high: 73,
|
||||
critical: 99,
|
||||
};
|
|
@ -6,6 +6,14 @@
|
|||
|
||||
import { AboutStepRule } from '../../types';
|
||||
|
||||
export const threatsDefault = [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: { id: 'none', name: 'none', reference: 'none' },
|
||||
techniques: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const stepAboutDefaultValue: AboutStepRule = {
|
||||
name: '',
|
||||
description: '',
|
||||
|
@ -15,11 +23,9 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
references: [''],
|
||||
falsePositives: [''],
|
||||
tags: [],
|
||||
threats: [
|
||||
{
|
||||
framework: 'MITRE ATT&CK',
|
||||
tactic: { id: 'none', name: 'none', reference: 'none' },
|
||||
techniques: [],
|
||||
},
|
||||
],
|
||||
timeline: {
|
||||
id: null,
|
||||
title: null,
|
||||
},
|
||||
threats: threatsDefault,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
const urlExpression = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi;
|
||||
|
||||
export const isUrlInvalid = (url: string | null | undefined) => {
|
||||
if (!isEmpty(url) && url != null && url.match(urlExpression) == null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -7,17 +7,21 @@
|
|||
import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { isEqual, get } from 'lodash/fp';
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
|
||||
import * as RuleI18n from '../../translations';
|
||||
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
|
||||
import { AddItem } from '../add_item_form';
|
||||
import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data';
|
||||
import { stepAboutDefaultValue } from './default_value';
|
||||
import { schema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
import { StepRuleDescription } from '../description_step';
|
||||
import { AddMitreThreat } from '../mitre';
|
||||
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
|
||||
|
||||
import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data';
|
||||
import { stepAboutDefaultValue } from './default_value';
|
||||
import { isUrlInvalid } from './helpers';
|
||||
import { schema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
import { PickTimeline } from '../pick_timeline';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -25,6 +29,10 @@ interface StepAboutRuleProps extends RuleStepProps {
|
|||
defaultValues?: AboutStepRule | null;
|
||||
}
|
||||
|
||||
const TagContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
export const StepAboutRule = memo<StepAboutRuleProps>(
|
||||
({
|
||||
defaultValues,
|
||||
|
@ -90,7 +98,6 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
idAria: 'detectionEngineStepAboutRuleName',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleName',
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
},
|
||||
|
@ -99,11 +106,9 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
<CommonUseField
|
||||
path="description"
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
idAria: 'detectionEngineStepAboutRuleDescription',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleDescription',
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
disabled: isLoading,
|
||||
},
|
||||
}}
|
||||
|
@ -114,7 +119,6 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
idAria: 'detectionEngineStepAboutRuleSeverity',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleSeverity',
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
options: severityOptions,
|
||||
|
@ -129,29 +133,38 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
euiFieldProps: {
|
||||
max: 100,
|
||||
min: 0,
|
||||
compressed: true,
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
options: severityOptions,
|
||||
showTicks: true,
|
||||
tickInterval: 25,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="timeline"
|
||||
component={PickTimeline}
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleTimeline',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepAboutRuleTimeline',
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="references"
|
||||
component={AddItem}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
addText: I18n.ADD_REFERENCE,
|
||||
idAria: 'detectionEngineStepAboutRuleReferenceUrls',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepAboutRuleReferenceUrls',
|
||||
validate: isUrlInvalid,
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="falsePositives"
|
||||
component={AddItem}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
addText: I18n.ADD_FALSE_POSITIVE,
|
||||
idAria: 'detectionEngineStepAboutRuleFalsePositives',
|
||||
isDisabled: isLoading,
|
||||
|
@ -162,24 +175,25 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
path="threats"
|
||||
component={AddMitreThreat}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
idAria: 'detectionEngineStepAboutRuleMitreThreats',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepAboutRuleMitreThreats',
|
||||
}}
|
||||
/>
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleTags',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleTags',
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
fullWidth: true,
|
||||
isDisabled: isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TagContainer>
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepAboutRuleTags',
|
||||
'data-test-subj': 'detectionEngineStepAboutRuleTags',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
isDisabled: isLoading,
|
||||
placeholder: '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TagContainer>
|
||||
<FormDataProvider pathsToWatch="severity">
|
||||
{({ severity }) => {
|
||||
const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue];
|
||||
|
@ -202,7 +216,7 @@ export const StepAboutRule = memo<StepAboutRuleProps>(
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
|
||||
{myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE}
|
||||
{RuleI18n.CONTINUE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import * as RuleI18n from '../../translations';
|
||||
|
@ -18,6 +17,8 @@ import {
|
|||
ValidationFunc,
|
||||
ERROR_CODE,
|
||||
} from '../shared_imports';
|
||||
import { isMitreAttackInvalid } from '../mitre/helpers';
|
||||
import { isUrlInvalid } from './helpers';
|
||||
import * as I18n from './translations';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
@ -63,7 +64,7 @@ export const schema: FormSchema = {
|
|||
],
|
||||
},
|
||||
severity: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
type: FIELD_TYPES.SUPER_SELECT,
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel',
|
||||
{
|
||||
|
@ -92,6 +93,14 @@ export const schema: FormSchema = {
|
|||
}
|
||||
),
|
||||
},
|
||||
timeline: {
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel',
|
||||
{
|
||||
defaultMessage: 'Timeline template',
|
||||
}
|
||||
),
|
||||
},
|
||||
references: {
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel',
|
||||
|
@ -100,6 +109,28 @@ export const schema: FormSchema = {
|
|||
}
|
||||
),
|
||||
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path }] = args;
|
||||
let hasError = false;
|
||||
(value as string[]).forEach(url => {
|
||||
if (isUrlInvalid(url)) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
return hasError
|
||||
? {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path,
|
||||
message: I18n.URL_FORMAT_INVALID,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
falsePositives: {
|
||||
label: i18n.translate(
|
||||
|
@ -126,7 +157,7 @@ export const schema: FormSchema = {
|
|||
const [{ value, path }] = args;
|
||||
let hasError = false;
|
||||
(value as IMitreEnterpriseAttack[]).forEach(v => {
|
||||
if (isEmpty(v.tactic.name) || (v.tactic.name !== 'none' && isEmpty(v.techniques))) {
|
||||
if (isMitreAttackInvalid(v.tactic.name, v.techniques)) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
@ -146,6 +177,13 @@ export const schema: FormSchema = {
|
|||
label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
helpText: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.',
|
||||
}
|
||||
),
|
||||
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -54,3 +54,10 @@ export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate(
|
|||
defaultMessage: 'At least one Technique is required with a Tactic.',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_FORMAT_INVALID = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError',
|
||||
{
|
||||
defaultMessage: 'Url is invalid format',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,8 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import { isEqual, get } from 'lodash/fp';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty, isEqual, get } from 'lodash/fp';
|
||||
import React, { memo, useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public';
|
||||
|
@ -18,7 +24,7 @@ import { StepRuleDescription } from '../description_step';
|
|||
import { QueryBarDefineRule } from '../query_bar';
|
||||
import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports';
|
||||
import { schema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -34,7 +40,6 @@ const stepDefineDefaultValue = {
|
|||
filters: [],
|
||||
saved_id: null,
|
||||
},
|
||||
useIndicesConfig: 'true',
|
||||
};
|
||||
|
||||
const getStepDefaultValue = (
|
||||
|
@ -45,7 +50,6 @@ const getStepDefaultValue = (
|
|||
return {
|
||||
...defaultValues,
|
||||
isNew: false,
|
||||
useIndicesConfig: `${isEqual(defaultValues.index, indicesConfig)}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
@ -66,13 +70,22 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
|
|||
setForm,
|
||||
setStepData,
|
||||
}) => {
|
||||
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState('');
|
||||
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
|
||||
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false);
|
||||
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
|
||||
const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState(
|
||||
defaultValues != null ? defaultValues.index : indicesConfig ?? []
|
||||
);
|
||||
const [
|
||||
{ indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar },
|
||||
setIndices,
|
||||
] = useFetchIndexPatterns(defaultValues != null ? defaultValues.index : indicesConfig ?? []);
|
||||
const [myStepData, setMyStepData] = useState<DefineStepRule>(stepDefineDefaultValue);
|
||||
{
|
||||
browserFields,
|
||||
indexPatterns: indexPatternQueryBar,
|
||||
isLoading: indexPatternLoadingQueryBar,
|
||||
},
|
||||
] = useFetchIndexPatterns(mylocalIndicesConfig);
|
||||
const [myStepData, setMyStepData] = useState<DefineStepRule>(
|
||||
getStepDefaultValue(indicesConfig, null)
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
defaultValue: myStepData,
|
||||
|
@ -96,7 +109,7 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
|
|||
const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues);
|
||||
if (!isEqual(myDefaultValues, myStepData)) {
|
||||
setMyStepData(myDefaultValues);
|
||||
setLocalUseIndicesConfig(myDefaultValues.useIndicesConfig);
|
||||
setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig));
|
||||
if (!isReadOnlyView) {
|
||||
Object.keys(schema).forEach(key => {
|
||||
const val = get(key, myDefaultValues);
|
||||
|
@ -115,6 +128,19 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
|
|||
}
|
||||
}, [form]);
|
||||
|
||||
const handleResetIndices = useCallback(() => {
|
||||
const indexField = form.getFields().index;
|
||||
indexField.setValue(indicesConfig);
|
||||
}, [form, indicesConfig]);
|
||||
|
||||
const handleOpenTimelineSearch = useCallback(() => {
|
||||
setOpenTimelineSearch(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseTimelineSearch = useCallback(() => {
|
||||
setOpenTimelineSearch(false);
|
||||
}, []);
|
||||
|
||||
return isReadOnlyView && myStepData != null ? (
|
||||
<StepRuleDescription
|
||||
direction={descriptionDirection}
|
||||
|
@ -125,74 +151,63 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
|
|||
) : (
|
||||
<>
|
||||
<Form form={form} data-test-subj="stepDefineRule">
|
||||
<CommonUseField
|
||||
path="useIndicesConfig"
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepDefineRuleUseIndicesConfig',
|
||||
'data-test-subj': 'detectionEngineStepDefineRuleUseIndicesConfig',
|
||||
euiFieldProps: {
|
||||
disabled: isLoading,
|
||||
options: [
|
||||
{
|
||||
id: 'true',
|
||||
label: I18n.CONFIG_INDICES,
|
||||
},
|
||||
{
|
||||
id: 'false',
|
||||
label: I18n.CUSTOM_INDICES,
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<CommonUseField
|
||||
path="index"
|
||||
config={{
|
||||
...schema.index,
|
||||
labelAppend: !localUseIndicesConfig ? (
|
||||
<EuiButtonEmpty size="xs" onClick={handleResetIndices}>
|
||||
<small>{i18n.RESET_DEFAULT_INDEX}</small>
|
||||
</EuiButtonEmpty>
|
||||
) : null,
|
||||
}}
|
||||
componentProps={{
|
||||
idAria: 'detectionEngineStepDefineRuleIndices',
|
||||
'data-test-subj': 'detectionEngineStepDefineRuleIndices',
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
fullWidth: true,
|
||||
isDisabled: isLoading,
|
||||
placeholder: '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="queryBar"
|
||||
config={{
|
||||
...schema.queryBar,
|
||||
labelAppend: (
|
||||
<EuiButtonEmpty size="xs" onClick={handleOpenTimelineSearch}>
|
||||
<small>{i18n.IMPORT_TIMELINE_QUERY}</small>
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
}}
|
||||
component={QueryBarDefineRule}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
browserFields,
|
||||
loading: indexPatternLoadingQueryBar,
|
||||
idAria: 'detectionEngineStepDefineRuleQueryBar',
|
||||
indexPattern: indexPatternQueryBar,
|
||||
isDisabled: isLoading,
|
||||
isLoading: indexPatternLoadingQueryBar,
|
||||
dataTestSubj: 'detectionEngineStepDefineRuleQueryBar',
|
||||
openTimelineSearch,
|
||||
onCloseTimelineSearch: handleCloseTimelineSearch,
|
||||
resizeParentContainer,
|
||||
}}
|
||||
/>
|
||||
<FormDataProvider pathsToWatch="useIndicesConfig">
|
||||
{({ useIndicesConfig }) => {
|
||||
if (localUseIndicesConfig !== useIndicesConfig) {
|
||||
const indexField = form.getFields().index;
|
||||
if (
|
||||
indexField != null &&
|
||||
useIndicesConfig === 'true' &&
|
||||
!isEqual(indexField.value, indicesConfig)
|
||||
) {
|
||||
indexField.setValue(indicesConfig);
|
||||
setIndices(indicesConfig);
|
||||
} else if (
|
||||
indexField != null &&
|
||||
useIndicesConfig === 'false' &&
|
||||
isEqual(indexField.value, indicesConfig)
|
||||
) {
|
||||
indexField.setValue([]);
|
||||
setIndices([]);
|
||||
<FormDataProvider pathsToWatch="index">
|
||||
{({ index }) => {
|
||||
if (index != null) {
|
||||
if (isEqual(index, indicesConfig) && !localUseIndicesConfig) {
|
||||
setLocalUseIndicesConfig(true);
|
||||
}
|
||||
if (!isEqual(index, indicesConfig) && localUseIndicesConfig) {
|
||||
setLocalUseIndicesConfig(false);
|
||||
}
|
||||
if (index != null && !isEmpty(index) && !isEqual(index, mylocalIndicesConfig)) {
|
||||
setMyLocalIndicesConfig(index);
|
||||
}
|
||||
setLocalUseIndicesConfig(useIndicesConfig);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
|
@ -208,7 +223,7 @@ export const StepDefineRule = memo<StepDefineRuleProps>(
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={onSubmit} isDisabled={isLoading}>
|
||||
{myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE}
|
||||
{RuleI18n.CONTINUE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -10,7 +10,6 @@ import { isEmpty } from 'lodash/fp';
|
|||
import React from 'react';
|
||||
|
||||
import { esKuery } from '../../../../../../../../../../src/plugins/data/public';
|
||||
import * as RuleI18n from '../../translations';
|
||||
import { FieldValueQueryBar } from '../query_bar';
|
||||
import {
|
||||
ERROR_CODE,
|
||||
|
@ -19,33 +18,27 @@ import {
|
|||
FormSchema,
|
||||
ValidationFunc,
|
||||
} from '../shared_imports';
|
||||
import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations';
|
||||
import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
export const schema: FormSchema = {
|
||||
useIndicesConfig: {
|
||||
type: FIELD_TYPES.RADIO_GROUP,
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldIndicesTypeLabel',
|
||||
{
|
||||
defaultMessage: 'Indices type',
|
||||
}
|
||||
),
|
||||
},
|
||||
index: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
labelAppend: <EuiText size="xs">{RuleI18n.OPTIONAL_FIELD}</EuiText>,
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel',
|
||||
{
|
||||
defaultMessage: 'Index patterns',
|
||||
}
|
||||
),
|
||||
helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'An output indice name for signals is required.',
|
||||
defaultMessage: 'Index patterns for signals is required.',
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
|
@ -33,3 +33,25 @@ export const CUSTOM_INDICES = i18n.translate(
|
|||
defaultMessage: 'Provide custom list of indices',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_HELPER_TEXT = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesHelperDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in SIEM advanced settings.',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESET_DEFAULT_INDEX = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton',
|
||||
{
|
||||
defaultMessage: 'Reset to default index patterns',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_TIMELINE_QUERY = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton',
|
||||
{
|
||||
defaultMessage: 'Import query from saved timeline',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -92,7 +92,6 @@ export const StepScheduleRule = memo<StepScheduleRuleProps>(
|
|||
path="interval"
|
||||
component={ScheduleItem}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
idAria: 'detectionEngineStepScheduleRuleInterval',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepScheduleRuleInterval',
|
||||
|
@ -102,7 +101,6 @@ export const StepScheduleRule = memo<StepScheduleRuleProps>(
|
|||
path="from"
|
||||
component={ScheduleItem}
|
||||
componentProps={{
|
||||
compressed: true,
|
||||
idAria: 'detectionEngineStepScheduleRuleFrom',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepScheduleRuleFrom',
|
||||
|
|
|
@ -40,14 +40,14 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => {
|
|||
};
|
||||
|
||||
const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => {
|
||||
const { queryBar, useIndicesConfig, isNew, ...rest } = defineStepData;
|
||||
const { queryBar, isNew, ...rest } = defineStepData;
|
||||
const { filters, query, saved_id: savedId } = queryBar;
|
||||
return {
|
||||
...rest,
|
||||
language: query.language,
|
||||
filters,
|
||||
query: query.query as string,
|
||||
...(savedId != null ? { saved_id: savedId } : {}),
|
||||
...(savedId != null && savedId !== '' ? { saved_id: savedId } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -72,11 +72,21 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul
|
|||
};
|
||||
|
||||
const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => {
|
||||
const { falsePositives, references, riskScore, threats, isNew, ...rest } = aboutStepData;
|
||||
const {
|
||||
falsePositives,
|
||||
references,
|
||||
riskScore,
|
||||
threats,
|
||||
timeline,
|
||||
isNew,
|
||||
...rest
|
||||
} = aboutStepData;
|
||||
return {
|
||||
false_positives: falsePositives.filter(item => !isEmpty(item)),
|
||||
references: references.filter(item => !isEmpty(item)),
|
||||
risk_score: riskScore,
|
||||
timeline_id: timeline.id,
|
||||
timeline_title: timeline.title,
|
||||
threats: threats
|
||||
.filter(threat => threat.tactic.name !== 'none')
|
||||
.map(threat => ({
|
||||
|
@ -97,7 +107,7 @@ export const formatRule = (
|
|||
scheduleData: ScheduleStepRule,
|
||||
ruleId?: string
|
||||
): NewRule => {
|
||||
const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query';
|
||||
const type: FormatRuleType = !isEmpty(defineStepData.queryBar.saved_id) ? 'saved_query' : 'query';
|
||||
const persistData = {
|
||||
type,
|
||||
...formatDefineStepData(defineStepData),
|
||||
|
|
|
@ -12,12 +12,13 @@ import styled from 'styled-components';
|
|||
import { HeaderPage } from '../../../../components/header_page';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
import { WrapperPage } from '../../../../components/wrapper_page';
|
||||
import { usePersistRule } from '../../../../containers/detection_engine/rules';
|
||||
import { SpyRoute } from '../../../../utils/route/spy_routes';
|
||||
import { AccordionTitle } from '../components/accordion_title';
|
||||
import { FormData, FormHook } from '../components/shared_imports';
|
||||
import { StepAboutRule } from '../components/step_about_rule';
|
||||
import { StepDefineRule } from '../components/step_define_rule';
|
||||
import { StepScheduleRule } from '../components/step_schedule_rule';
|
||||
import { usePersistRule } from '../../../../containers/detection_engine/rules';
|
||||
import { SpyRoute } from '../../../../utils/route/spy_routes';
|
||||
import * as RuleI18n from '../translations';
|
||||
import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types';
|
||||
import { formatRule } from './helpers';
|
||||
|
@ -28,17 +29,43 @@ const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.schedu
|
|||
const ResizeEuiPanel = styled(EuiPanel)<{
|
||||
height?: number;
|
||||
}>`
|
||||
.euiAccordion__iconWrapper {
|
||||
display: none;
|
||||
}
|
||||
.euiAccordion__childWrapper {
|
||||
height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')};
|
||||
}
|
||||
.euiAccordion__button {
|
||||
cursor: default !important;
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MyEuiPanel = styled(EuiPanel)`
|
||||
.euiAccordion__iconWrapper {
|
||||
display: none;
|
||||
}
|
||||
.euiAccordion__button {
|
||||
cursor: default !important;
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CreateRuleComponent = React.memo(() => {
|
||||
const [heightAccordion, setHeightAccordion] = useState(-1);
|
||||
const [openAccordionId, setOpenAccordionId] = useState<RuleStep | null>(RuleStep.defineRule);
|
||||
const [openAccordionId, setOpenAccordionId] = useState<RuleStep>(RuleStep.defineRule);
|
||||
const defineRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const aboutRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const scheduleRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
|
||||
[RuleStep.defineRule]: null,
|
||||
[RuleStep.aboutRule]: null,
|
||||
[RuleStep.scheduleRule]: null,
|
||||
});
|
||||
const stepsData = useRef<Record<RuleStep, RuleStepData>>({
|
||||
[RuleStep.defineRule]: { isValid: false, data: {} },
|
||||
[RuleStep.aboutRule]: { isValid: false, data: {} },
|
||||
|
@ -57,11 +84,17 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
if (isValid) {
|
||||
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
|
||||
if ([0, 1].includes(stepRuleIdx)) {
|
||||
setIsStepRuleInEditView({
|
||||
...isStepRuleInReadOnlyView,
|
||||
[step]: true,
|
||||
});
|
||||
if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) {
|
||||
if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) {
|
||||
setIsStepRuleInEditView({
|
||||
...isStepRuleInReadOnlyView,
|
||||
[step]: true,
|
||||
[stepsRuleOrder[stepRuleIdx + 1]]: false,
|
||||
});
|
||||
} else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) {
|
||||
setIsStepRuleInEditView({
|
||||
...isStepRuleInReadOnlyView,
|
||||
[step]: true,
|
||||
});
|
||||
openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]);
|
||||
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
|
||||
}
|
||||
|
@ -80,9 +113,13 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
}
|
||||
}
|
||||
},
|
||||
[openAccordionId, stepsData.current, setRule]
|
||||
[isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule]
|
||||
);
|
||||
|
||||
const setStepsForm = useCallback((step: RuleStep, form: FormHook<FormData>) => {
|
||||
stepsForm.current[step] = form;
|
||||
}, []);
|
||||
|
||||
const getAccordionType = useCallback(
|
||||
(accordionId: RuleStep) => {
|
||||
if (accordionId === openAccordionId) {
|
||||
|
@ -135,42 +172,38 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
(id: RuleStep, isOpen: boolean) => {
|
||||
const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId);
|
||||
const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id);
|
||||
const isLatestStepsRuleValid =
|
||||
stepRuleIdx === 0
|
||||
? true
|
||||
: stepsRuleOrder
|
||||
.filter((stepRule, index) => index < stepRuleIdx)
|
||||
.every(stepRule => stepsData.current[stepRule].isValid);
|
||||
|
||||
if (stepRuleIdx < activeRuleIdx && !isOpen) {
|
||||
if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) {
|
||||
openCloseAccordion(id);
|
||||
} else if (stepRuleIdx >= activeRuleIdx) {
|
||||
if (
|
||||
openAccordionId != null &&
|
||||
openAccordionId !== id &&
|
||||
!stepsData.current[openAccordionId].isValid &&
|
||||
!isStepRuleInReadOnlyView[id] &&
|
||||
isOpen
|
||||
) {
|
||||
openCloseAccordion(id);
|
||||
} else if (!isLatestStepsRuleValid && isOpen) {
|
||||
openCloseAccordion(id);
|
||||
} else if (id !== openAccordionId && isOpen) {
|
||||
setOpenAccordionId(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isStepRuleInReadOnlyView, openAccordionId]
|
||||
[isStepRuleInReadOnlyView, openAccordionId, stepsData]
|
||||
);
|
||||
|
||||
const manageIsEditable = useCallback(
|
||||
(id: RuleStep) => {
|
||||
setIsStepRuleInEditView({
|
||||
...isStepRuleInReadOnlyView,
|
||||
[id]: false,
|
||||
});
|
||||
async (id: RuleStep) => {
|
||||
const activeForm = await stepsForm.current[openAccordionId]?.submit();
|
||||
if (activeForm != null && activeForm?.isValid) {
|
||||
setOpenAccordionId(id);
|
||||
openCloseAccordion(openAccordionId);
|
||||
|
||||
setIsStepRuleInEditView({
|
||||
...isStepRuleInReadOnlyView,
|
||||
[openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true,
|
||||
[id]: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isStepRuleInReadOnlyView]
|
||||
[isStepRuleInReadOnlyView, openAccordionId]
|
||||
);
|
||||
|
||||
if (isSaved) {
|
||||
|
@ -201,7 +234,7 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
size="xs"
|
||||
onClick={manageIsEditable.bind(null, RuleStep.defineRule)}
|
||||
>
|
||||
{`Edit`}
|
||||
{i18n.EDIT_RULE}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
}
|
||||
|
@ -210,13 +243,14 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
<StepDefineRule
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
|
||||
isLoading={isLoading}
|
||||
setForm={setStepsForm}
|
||||
setStepData={setStepData}
|
||||
resizeParentContainer={height => setHeightAccordion(height)}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</ResizeEuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel>
|
||||
<MyEuiPanel>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
id={RuleStep.aboutRule}
|
||||
|
@ -231,7 +265,7 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
size="xs"
|
||||
onClick={manageIsEditable.bind(null, RuleStep.aboutRule)}
|
||||
>
|
||||
{`Edit`}
|
||||
{i18n.EDIT_RULE}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
}
|
||||
|
@ -240,12 +274,13 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
<StepAboutRule
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
|
||||
isLoading={isLoading}
|
||||
setForm={setStepsForm}
|
||||
setStepData={setStepData}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</MyEuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel>
|
||||
<MyEuiPanel>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
id={RuleStep.scheduleRule}
|
||||
|
@ -260,7 +295,7 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
size="xs"
|
||||
onClick={manageIsEditable.bind(null, RuleStep.scheduleRule)}
|
||||
>
|
||||
{`Edit`}
|
||||
{i18n.EDIT_RULE}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
}
|
||||
|
@ -269,10 +304,11 @@ export const CreateRuleComponent = React.memo(() => {
|
|||
<StepScheduleRule
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
|
||||
isLoading={isLoading}
|
||||
setForm={setStepsForm}
|
||||
setStepData={setStepData}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</MyEuiPanel>
|
||||
</WrapperPage>
|
||||
|
||||
<SpyRoute />
|
||||
|
|
|
@ -9,3 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', {
|
||||
defaultMessage: 'Create new rule',
|
||||
});
|
||||
|
||||
export const EDIT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.editRuleButton', {
|
||||
defaultMessage: 'Edit',
|
||||
});
|
||||
|
|
|
@ -113,7 +113,10 @@ export const RuleDetailsComponent = memo<RuleDetailsComponentProps>(({ signalsIn
|
|||
|
||||
<WrapperPage>
|
||||
<HeaderPage
|
||||
backOptions={{ href: '#detection-engine/rules', text: i18n.BACK_TO_RULES }}
|
||||
backOptions={{
|
||||
href: `#${DETECTION_ENGINE_PAGE_NAME}/rules`,
|
||||
text: i18n.BACK_TO_RULES,
|
||||
}}
|
||||
badgeOptions={{ text: i18n.EXPERIMENTAL }}
|
||||
border
|
||||
subtitle={subTitle}
|
||||
|
@ -142,7 +145,7 @@ export const RuleDetailsComponent = memo<RuleDetailsComponentProps>(({ signalsIn
|
|||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}/edit`}
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}/edit`}
|
||||
iconType="visControls"
|
||||
isDisabled={rule?.immutable ?? true}
|
||||
>
|
||||
|
|
|
@ -49,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm {
|
|||
export const EditRuleComponent = memo(() => {
|
||||
const { ruleId } = useParams();
|
||||
const [loading, rule] = useRule(ruleId);
|
||||
|
||||
const [initForm, setInitForm] = useState(false);
|
||||
const [myAboutRuleForm, setMyAboutRuleForm] = useState<AboutStepRuleForm>({
|
||||
data: null,
|
||||
|
@ -249,7 +250,7 @@ export const EditRuleComponent = memo(() => {
|
|||
}, []);
|
||||
|
||||
if (isSaved || (rule != null && rule.immutable)) {
|
||||
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`} />;
|
||||
return <Redirect to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -257,7 +258,7 @@ export const EditRuleComponent = memo(() => {
|
|||
<WrapperPage restrictWidth>
|
||||
<HeaderPage
|
||||
backOptions={{
|
||||
href: `#/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`,
|
||||
href: `#/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`,
|
||||
text: `${i18n.BACK_TO} ${rule?.name ?? ''}`,
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
|
@ -303,7 +304,7 @@ export const EditRuleComponent = memo(() => {
|
|||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton iconType="cross" href={`#/${DETECTION_ENGINE_PAGE_NAME}/rules/${ruleId}`}>
|
||||
<EuiButton iconType="cross" href={`#/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${ruleId}`}>
|
||||
{i18n.CANCEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -33,7 +33,6 @@ export const getStepsData = ({
|
|||
filters: rule.filters as esFilters.Filter[],
|
||||
saved_id: rule.saved_id ?? null,
|
||||
},
|
||||
useIndicesConfig: 'true',
|
||||
}
|
||||
: null;
|
||||
const aboutRuleData: AboutStepRule | null =
|
||||
|
@ -45,6 +44,10 @@ export const getStepsData = ({
|
|||
threats: rule.threats as IMitreEnterpriseAttack[],
|
||||
falsePositives: rule.false_positives,
|
||||
riskScore: rule.risk_score,
|
||||
timeline: {
|
||||
id: rule.timeline_id ?? null,
|
||||
title: rule.timeline_title ?? null,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
const scheduleRuleData: ScheduleStepRule | null =
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine';
|
||||
import { FormattedRelativePreferenceDate } from '../../../components/formatted_date';
|
||||
import { getEmptyTagValue } from '../../../components/empty_value';
|
||||
import { HeaderPage } from '../../../components/header_page';
|
||||
|
@ -32,7 +33,10 @@ export const RulesComponent = React.memo(() => {
|
|||
/>
|
||||
<WrapperPage>
|
||||
<HeaderPage
|
||||
backOptions={{ href: '#detection-engine', text: i18n.BACK_TO_DETECTION_ENGINE }}
|
||||
backOptions={{
|
||||
href: `#${DETECTION_ENGINE_PAGE_NAME}`,
|
||||
text: i18n.BACK_TO_DETECTION_ENGINE,
|
||||
}}
|
||||
subtitle={
|
||||
lastCompletedRun ? (
|
||||
<FormattedMessage
|
||||
|
@ -61,7 +65,11 @@ export const RulesComponent = React.memo(() => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill href="#/detection-engine/rules/create" iconType="plusInCircle">
|
||||
<EuiButton
|
||||
fill
|
||||
href={`#${DETECTION_ENGINE_PAGE_NAME}/rules/create`}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{i18n.ADD_NEW_RULE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/common';
|
|||
import { Rule } from '../../../containers/detection_engine/rules';
|
||||
import { FieldValueQueryBar } from './components/query_bar';
|
||||
import { FormData, FormHook } from './components/shared_imports';
|
||||
import { FieldValueTimeline } from './components/pick_timeline';
|
||||
|
||||
export interface EuiBasicTableSortTypes {
|
||||
field: string;
|
||||
|
@ -76,11 +77,11 @@ export interface AboutStepRule extends StepRuleData {
|
|||
references: string[];
|
||||
falsePositives: string[];
|
||||
tags: string[];
|
||||
timeline: FieldValueTimeline;
|
||||
threats: IMitreEnterpriseAttack[];
|
||||
}
|
||||
|
||||
export interface DefineStepRule extends StepRuleData {
|
||||
useIndicesConfig: string;
|
||||
index: string[];
|
||||
queryBar: FieldValueQueryBar;
|
||||
}
|
||||
|
@ -108,6 +109,8 @@ export interface AboutStepRuleJson {
|
|||
references: string[];
|
||||
false_positives: string[];
|
||||
tags: string[];
|
||||
timeline_id: string | null;
|
||||
timeline_title: string | null;
|
||||
threats: IMitreEnterpriseAttack[];
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({
|
|||
created_at: '2019-12-13T16:40:33.400Z',
|
||||
updated_at: '2019-12-13T16:40:33.400Z',
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: 'timeline-title',
|
||||
});
|
||||
|
||||
export const typicalPayload = (): Partial<RuleAlertParamsRest> => ({
|
||||
|
@ -271,6 +272,7 @@ export const getResult = (): RuleAlertType => ({
|
|||
outputIndex: '.siem-signals',
|
||||
savedId: 'some-id',
|
||||
timelineId: 'some-timeline-id',
|
||||
timelineTitle: 'some-timeline-title',
|
||||
meta: { someMeta: 'someField' },
|
||||
filters: [
|
||||
{
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"timeline_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeline_title": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"max_signals": {
|
||||
"type": "keyword"
|
||||
},
|
||||
|
|
|
@ -74,6 +74,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
updated_at: updatedAt,
|
||||
references,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
version,
|
||||
} = payloadRule;
|
||||
const ruleIdOrUuid = ruleId ?? uuid.v4();
|
||||
|
@ -112,6 +113,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
outputIndex: finalIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId: ruleIdOrUuid,
|
||||
|
|
|
@ -44,6 +44,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
|
@ -101,6 +102,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
outputIndex: finalIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId: ruleId != null ? ruleId : uuid.v4(),
|
||||
|
|
|
@ -50,6 +50,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
|
@ -82,6 +83,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
id,
|
||||
|
|
|
@ -38,6 +38,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = {
|
|||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
|
@ -77,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = {
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
id,
|
||||
|
|
|
@ -79,6 +79,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -141,6 +142,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -205,6 +207,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -269,6 +272,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -331,6 +335,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -396,6 +401,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -461,6 +467,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -526,6 +533,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
version: 1,
|
||||
|
@ -642,6 +650,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
version: 1,
|
||||
};
|
||||
expect(output).toEqual({
|
||||
|
@ -714,6 +723,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
version: 1,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
|
@ -875,6 +885,7 @@ describe('utils', () => {
|
|||
},
|
||||
saved_id: 'some-id',
|
||||
timeline_id: 'some-timeline-id',
|
||||
timeline_title: 'some-timeline-title',
|
||||
version: 1,
|
||||
};
|
||||
expect(output).toEqual(expected);
|
||||
|
|
|
@ -85,6 +85,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial<OutputRuleAl
|
|||
references: alert.params.references,
|
||||
saved_id: alert.params.savedId,
|
||||
timeline_id: alert.params.timelineId,
|
||||
timeline_title: alert.params.timelineTitle,
|
||||
meta: alert.params.meta,
|
||||
severity: alert.params.severity,
|
||||
updated_by: alert.updatedBy,
|
||||
|
|
|
@ -1076,9 +1076,7 @@ describe('add prepackaged rules schema', () => {
|
|||
|
||||
test('You can omit the query string when filters are present', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<
|
||||
Partial<Omit<RuleAlertParamsRest, 'meta'> & { meta: string }>
|
||||
>({
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
|
@ -1099,7 +1097,7 @@ describe('add prepackaged rules schema', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validates with timeline_id', () => {
|
||||
test('validates with timeline_id and timeline_title', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
|
@ -1117,7 +1115,131 @@ describe('add prepackaged rules schema', () => {
|
|||
language: 'kuery',
|
||||
version: 1,
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: 'timeline-title',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('You cannot omit timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
immutable: true,
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
max_signals: 1,
|
||||
version: 1,
|
||||
timeline_id: 'timeline-id',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
immutable: true,
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
max_signals: 1,
|
||||
version: 1,
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: null,
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
immutable: true,
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
max_signals: 1,
|
||||
version: 1,
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: '',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title with an empty timeline_id', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
immutable: true,
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
max_signals: 1,
|
||||
version: 1,
|
||||
timeline_id: '',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title without timeline_id', () => {
|
||||
expect(
|
||||
addPrepackagedRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
immutable: true,
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
language: 'kuery',
|
||||
filters: [],
|
||||
max_signals: 1,
|
||||
version: 1,
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
language,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
|
@ -63,6 +64,7 @@ export const addPrepackagedRulesSchema = Joi.object({
|
|||
otherwise: Joi.forbidden(),
|
||||
}),
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score: risk_score.required(),
|
||||
max_signals: max_signals.default(DEFAULT_MAX_SIGNALS),
|
||||
|
|
|
@ -1024,7 +1024,7 @@ describe('create rules schema', () => {
|
|||
|
||||
test('You can omit the query string when filters are present', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<Omit<RuleAlertParamsRest, 'meta'> & { meta: string }>>({
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
|
@ -1045,7 +1045,30 @@ describe('create rules schema', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('timeline_id validates', () => {
|
||||
test('validates with timeline_id and timeline_title', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
query: 'some query',
|
||||
language: 'kuery',
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: 'timeline-title',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('You cannot omit timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
|
@ -1064,6 +1087,97 @@ describe('create rules schema', () => {
|
|||
language: 'kuery',
|
||||
timeline_id: 'some_id',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
query: 'some query',
|
||||
language: 'kuery',
|
||||
timeline_id: 'some_id',
|
||||
timeline_title: null,
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
query: 'some query',
|
||||
language: 'kuery',
|
||||
timeline_id: 'some_id',
|
||||
timeline_title: '',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title with an empty timeline_id', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
query: 'some query',
|
||||
language: 'kuery',
|
||||
timeline_id: '',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title without timeline_id', () => {
|
||||
expect(
|
||||
createRulesSchema.validate<Partial<RuleAlertParamsRest>>({
|
||||
rule_id: 'rule-1',
|
||||
output_index: '.siem-signals',
|
||||
risk_score: 50,
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'query',
|
||||
references: ['index-1'],
|
||||
query: 'some query',
|
||||
language: 'kuery',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
|
@ -57,6 +58,7 @@ export const createRulesSchema = Joi.object({
|
|||
otherwise: Joi.forbidden(),
|
||||
}),
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score: risk_score.required(),
|
||||
max_signals: max_signals.default(DEFAULT_MAX_SIGNALS),
|
||||
|
|
|
@ -24,6 +24,11 @@ export const language = Joi.string().valid('kuery', 'lucene');
|
|||
export const output_index = Joi.string();
|
||||
export const saved_id = Joi.string();
|
||||
export const timeline_id = Joi.string();
|
||||
export const timeline_title = Joi.string().when('timeline_id', {
|
||||
is: Joi.exist(),
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.forbidden(),
|
||||
});
|
||||
export const meta = Joi.object();
|
||||
export const max_signals = Joi.number().greater(0);
|
||||
export const name = Joi.string();
|
||||
|
|
|
@ -867,7 +867,26 @@ describe('update rules schema', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('timeline_id validates', () => {
|
||||
test('validates with timeline_id and timeline_title', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'saved_query',
|
||||
saved_id: 'some id',
|
||||
timeline_id: 'some-id',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('You cannot omit timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
|
@ -882,6 +901,81 @@ describe('update rules schema', () => {
|
|||
saved_id: 'some id',
|
||||
timeline_id: 'some-id',
|
||||
}).error
|
||||
).toBeFalsy();
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'saved_query',
|
||||
saved_id: 'some id',
|
||||
timeline_id: 'timeline-id',
|
||||
timeline_title: null,
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'saved_query',
|
||||
saved_id: 'some id',
|
||||
timeline_id: 'some-id',
|
||||
timeline_title: '',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title with an empty timeline_id', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'saved_query',
|
||||
saved_id: 'some id',
|
||||
timeline_id: '',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('You cannot have timeline_title without timeline_id', () => {
|
||||
expect(
|
||||
updateRulesSchema.validate<Partial<UpdateRuleAlertParamsRest>>({
|
||||
id: 'rule-1',
|
||||
description: 'some description',
|
||||
from: 'now-5m',
|
||||
to: 'now',
|
||||
index: ['index-1'],
|
||||
name: 'some-name',
|
||||
severity: 'severity',
|
||||
interval: '5m',
|
||||
type: 'saved_query',
|
||||
saved_id: 'some id',
|
||||
timeline_title: 'some-title',
|
||||
}).error
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
|
@ -53,6 +54,7 @@ export const updateRulesSchema = Joi.object({
|
|||
output_index,
|
||||
saved_id,
|
||||
timeline_id,
|
||||
timeline_title,
|
||||
meta,
|
||||
risk_score,
|
||||
max_signals,
|
||||
|
|
|
@ -19,6 +19,7 @@ export const createRules = async ({
|
|||
language,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId,
|
||||
|
@ -56,6 +57,7 @@ export const createRules = async ({
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
maxSignals,
|
||||
|
|
|
@ -26,6 +26,7 @@ export const installPrepackagedRules = async (
|
|||
language,
|
||||
saved_id: savedId,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule_id: ruleId,
|
||||
|
@ -55,6 +56,7 @@ export const installPrepackagedRules = async (
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId,
|
||||
|
|
|
@ -74,6 +74,7 @@ export const updateRules = async ({
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
from,
|
||||
|
@ -118,6 +119,7 @@ export const updateRules = async ({
|
|||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
index,
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
"from": "now-6m",
|
||||
"to": "now",
|
||||
"query": "user.name: root or user.name: admin",
|
||||
"timeline_id": "timeline-id"
|
||||
"timeline_id": "timeline-id",
|
||||
"timeline_title": "timeline_title"
|
||||
}
|
||||
|
|
|
@ -78,5 +78,6 @@
|
|||
"Some plain text string here explaining why this is a valid thing to look out for"
|
||||
],
|
||||
"timeline_id": "timeline_id",
|
||||
"timeline_title": "timeline_title",
|
||||
"version": 1
|
||||
}
|
||||
|
|
|
@ -78,5 +78,6 @@
|
|||
"Some plain text string here explaining why this is a valid thing to look out for"
|
||||
],
|
||||
"saved_id": "test-saved-id",
|
||||
"timeline_id": "test-timeline-id"
|
||||
"timeline_id": "test-timeline-id",
|
||||
"timeline_title": "test-timeline-title"
|
||||
}
|
||||
|
|
|
@ -78,5 +78,6 @@
|
|||
"Some plain text string here explaining why this is a valid thing to look out for"
|
||||
],
|
||||
"timeline_id": "other-timeline-id",
|
||||
"timeline_title": "other-timeline-title",
|
||||
"version": 42
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"rule_id": "query-rule-id",
|
||||
"timeline_id": "other-timeline-id"
|
||||
"timeline_id": "other-timeline-id",
|
||||
"timeline_title": "other-timeline-title"
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const sampleRuleAlertParams = (
|
|||
filters: undefined,
|
||||
savedId: undefined,
|
||||
timelineId: undefined,
|
||||
timelineTitle: undefined,
|
||||
meta: undefined,
|
||||
threats: undefined,
|
||||
version: 1,
|
||||
|
|
|
@ -34,6 +34,7 @@ export const buildRule = ({
|
|||
false_positives: ruleParams.falsePositives,
|
||||
saved_id: ruleParams.savedId,
|
||||
timeline_id: ruleParams.timelineId,
|
||||
timeline_title: ruleParams.timelineTitle,
|
||||
meta: ruleParams.meta,
|
||||
max_signals: ruleParams.maxSignals,
|
||||
risk_score: ruleParams.riskScore,
|
||||
|
|
|
@ -42,6 +42,7 @@ export const signalRulesAlertType = ({
|
|||
outputIndex: schema.nullable(schema.string()),
|
||||
savedId: schema.nullable(schema.string()),
|
||||
timelineId: schema.nullable(schema.string()),
|
||||
timelineTitle: schema.nullable(schema.string()),
|
||||
meta: schema.nullable(schema.object({}, { allowUnknowns: true })),
|
||||
query: schema.nullable(schema.string()),
|
||||
filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface RuleAlertParams {
|
|||
tags: string[];
|
||||
to: string;
|
||||
timelineId: string | undefined | null;
|
||||
timelineTitle: string | undefined | null;
|
||||
threats: ThreatParams[] | undefined | null;
|
||||
type: 'query' | 'saved_query';
|
||||
version: number;
|
||||
|
@ -60,6 +61,7 @@ export type RuleAlertParamsRest = Omit<
|
|||
| 'savedId'
|
||||
| 'riskScore'
|
||||
| 'timelineId'
|
||||
| 'timelineTitle'
|
||||
| 'outputIndex'
|
||||
| 'updatedAt'
|
||||
| 'createdAt'
|
||||
|
@ -68,6 +70,7 @@ export type RuleAlertParamsRest = Omit<
|
|||
false_positives: RuleAlertParams['falsePositives'];
|
||||
saved_id: RuleAlertParams['savedId'];
|
||||
timeline_id: RuleAlertParams['timelineId'];
|
||||
timeline_title: RuleAlertParams['timelineTitle'];
|
||||
max_signals: RuleAlertParams['maxSignals'];
|
||||
risk_score: RuleAlertParams['riskScore'];
|
||||
output_index: RuleAlertParams['outputIndex'];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue