Skip to content

Commit 961d61f

Browse files
[Security Solution] Disable bulk actions for immutable timeline templates (#73687) (#74066)
* disablebulk actions for immutable timeline templates * make immutable timelines not selectable * hide selected count if timeline status is immutable Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 3a8e92d commit 961d61f

File tree

7 files changed

+204
-26
lines changed

7 files changed

+204
-26
lines changed

x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
326326
sortField={sortField}
327327
templateTimelineFilter={templateTimelineFilter}
328328
timelineType={timelineType}
329+
timelineStatus={timelineStatus}
329330
timelineFilter={timelineTabs}
330331
title={title}
331332
totalSearchResultsCount={totalCount}
@@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
356357
sortField={sortField}
357358
templateTimelineFilter={templateTimelineFilter}
358359
timelineType={timelineType}
360+
timelineStatus={timelineStatus}
359361
timelineFilter={timelineFilters}
360362
title={title}
361363
totalSearchResultsCount={totalCount}

x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table';
1717
import { mockTimelineResults } from '../../../common/mock/timeline_results';
1818
import { OpenTimeline } from './open_timeline';
1919
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants';
20-
import { TimelineType } from '../../../../common/types/timeline';
20+
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
2121

2222
jest.mock('../../../common/lib/kibana');
2323

@@ -50,6 +50,7 @@ describe('OpenTimeline', () => {
5050
sortField: DEFAULT_SORT_FIELD,
5151
title,
5252
timelineType: TimelineType.default,
53+
timelineStatus: TimelineStatus.active,
5354
templateTimelineFilter: [<div />],
5455
totalSearchResultsCount: mockSearchResults.length,
5556
});
@@ -263,4 +264,136 @@ describe('OpenTimeline', () => {
263264
`Showing: ${mockResults.length} timelines with "How was your day?"`
264265
);
265266
});
267+
268+
test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => {
269+
const defaultProps = {
270+
...getDefaultTestProps(mockResults),
271+
timelineStatus: TimelineStatus.active,
272+
};
273+
const wrapper = mountWithIntl(
274+
<ThemeProvider theme={theme}>
275+
<OpenTimeline {...defaultProps} />
276+
</ThemeProvider>
277+
);
278+
279+
expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true);
280+
});
281+
282+
test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => {
283+
const defaultProps = {
284+
...getDefaultTestProps(mockResults),
285+
timelineStatus: TimelineStatus.active,
286+
};
287+
const wrapper = mountWithIntl(
288+
<ThemeProvider theme={theme}>
289+
<OpenTimeline {...defaultProps} />
290+
</ThemeProvider>
291+
);
292+
293+
expect(
294+
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
295+
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
296+
});
297+
298+
test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => {
299+
const defaultProps = {
300+
...getDefaultTestProps(mockResults),
301+
timelineStatus: TimelineStatus.active,
302+
};
303+
const wrapper = mountWithIntl(
304+
<ThemeProvider theme={theme}>
305+
<OpenTimeline {...defaultProps} />
306+
</ThemeProvider>
307+
);
308+
309+
expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true);
310+
});
311+
312+
test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
313+
const defaultProps = {
314+
...getDefaultTestProps(mockResults),
315+
timelineStatus: TimelineStatus.immutable,
316+
};
317+
const wrapper = mountWithIntl(
318+
<ThemeProvider theme={theme}>
319+
<OpenTimeline {...defaultProps} />
320+
</ThemeProvider>
321+
);
322+
323+
expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false);
324+
});
325+
326+
test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
327+
const defaultProps = {
328+
...getDefaultTestProps(mockResults),
329+
timelineStatus: TimelineStatus.immutable,
330+
};
331+
const wrapper = mountWithIntl(
332+
<ThemeProvider theme={theme}>
333+
<OpenTimeline {...defaultProps} />
334+
</ThemeProvider>
335+
);
336+
337+
expect(
338+
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
339+
).toEqual(['createFrom', 'duplicate']);
340+
});
341+
342+
test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
343+
const defaultProps = {
344+
...getDefaultTestProps(mockResults),
345+
timelineStatus: TimelineStatus.immutable,
346+
};
347+
const wrapper = mountWithIntl(
348+
<ThemeProvider theme={theme}>
349+
<OpenTimeline {...defaultProps} />
350+
</ThemeProvider>
351+
);
352+
353+
expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false);
354+
});
355+
356+
test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => {
357+
const defaultProps = {
358+
...getDefaultTestProps(mockResults),
359+
timelineStatus: null,
360+
};
361+
const wrapper = mountWithIntl(
362+
<ThemeProvider theme={theme}>
363+
<OpenTimeline {...defaultProps} />
364+
</ThemeProvider>
365+
);
366+
367+
expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true);
368+
});
369+
370+
test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => {
371+
const defaultProps = {
372+
...getDefaultTestProps(mockResults),
373+
timelineStatus: null,
374+
};
375+
const wrapper = mountWithIntl(
376+
<ThemeProvider theme={theme}>
377+
<OpenTimeline {...defaultProps} />
378+
</ThemeProvider>
379+
);
380+
381+
expect(
382+
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
383+
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
384+
});
385+
386+
test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => {
387+
const defaultProps = {
388+
...getDefaultTestProps(mockResults),
389+
timelineStatus: null,
390+
};
391+
const wrapper = mountWithIntl(
392+
<ThemeProvider theme={theme}>
393+
<OpenTimeline {...defaultProps} />
394+
</ThemeProvider>
395+
);
396+
397+
expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true);
398+
});
266399
});

x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui';
88
import React, { useCallback, useMemo, useRef } from 'react';
99
import { FormattedMessage } from '@kbn/i18n/react';
1010

11-
import { TimelineType } from '../../../../common/types/timeline';
11+
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
1212
import { ImportDataModal } from '../../../common/components/import_data_modal';
1313
import {
1414
UtilityBarGroup,
@@ -55,6 +55,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
5555
setImportDataModalToggle,
5656
sortField,
5757
timelineType = TimelineType.default,
58+
timelineStatus,
5859
timelineFilter,
5960
templateTimelineFilter,
6061
totalSearchResultsCount,
@@ -140,19 +141,23 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
140141
}, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]);
141142

142143
const actionTimelineToShow = useMemo<ActionTimelineToShow[]>(() => {
143-
const timelineActions: ActionTimelineToShow[] = [
144-
'createFrom',
145-
'duplicate',
146-
'export',
147-
'selectable',
148-
];
144+
const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate'];
149145

150-
if (onDeleteSelected != null && deleteTimelines != null) {
146+
if (timelineStatus !== TimelineStatus.immutable) {
147+
timelineActions.push('export');
148+
timelineActions.push('selectable');
149+
}
150+
151+
if (
152+
onDeleteSelected != null &&
153+
deleteTimelines != null &&
154+
timelineStatus !== TimelineStatus.immutable
155+
) {
151156
timelineActions.push('delete');
152157
}
153158

154159
return timelineActions;
155-
}, [onDeleteSelected, deleteTimelines]);
160+
}, [onDeleteSelected, deleteTimelines, timelineStatus]);
156161

157162
const SearchRowContent = useMemo(() => <>{templateTimelineFilter}</>, [templateTimelineFilter]);
158163

@@ -206,20 +211,24 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
206211
</>
207212
</UtilityBarText>
208213
</UtilityBarGroup>
209-
210214
<UtilityBarGroup>
211-
<UtilityBarText>
212-
{timelineType === TimelineType.template
213-
? i18n.SELECTED_TEMPLATES(selectedItems.length)
214-
: i18n.SELECTED_TIMELINES(selectedItems.length)}
215-
</UtilityBarText>
216-
<UtilityBarAction
217-
iconSide="right"
218-
iconType="arrowDown"
219-
popoverContent={getBatchItemsPopoverContent}
220-
>
221-
{i18n.BATCH_ACTIONS}
222-
</UtilityBarAction>
215+
{timelineStatus !== TimelineStatus.immutable && (
216+
<>
217+
<UtilityBarText data-test-subj="selected-count">
218+
{timelineType === TimelineType.template
219+
? i18n.SELECTED_TEMPLATES(selectedItems.length)
220+
: i18n.SELECTED_TIMELINES(selectedItems.length)}
221+
</UtilityBarText>
222+
<UtilityBarAction
223+
iconSide="right"
224+
iconType="arrowDown"
225+
popoverContent={getBatchItemsPopoverContent}
226+
data-test-subj="utility-bar-action"
227+
>
228+
{i18n.BATCH_ACTIONS}
229+
</UtilityBarAction>
230+
</>
231+
)}
223232
<UtilityBarAction iconSide="right" iconType="refresh" onClick={onRefreshBtnClick}>
224233
{i18n.REFRESH}
225234
</UtilityBarAction>

x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table';
1717
import { mockTimelineResults } from '../../../../common/mock/timeline_results';
1818
import { OpenTimelineModalBody } from './open_timeline_modal_body';
1919
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
20-
import { TimelineType } from '../../../../../common/types/timeline';
20+
import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline';
2121

2222
jest.mock('../../../../common/lib/kibana');
2323

@@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => {
4848
sortDirection: DEFAULT_SORT_DIRECTION,
4949
sortField: DEFAULT_SORT_FIELD,
5050
timelineType: TimelineType.default,
51+
timelineStatus: TimelineStatus.active,
5152
templateTimelineFilter: [<div />],
5253
title,
5354
totalSearchResultsCount: mockSearchResults.length,

x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import React from 'react';
1212
import { ThemeProvider } from 'styled-components';
1313

1414
import '../../../../common/mock/match_media';
15+
1516
import { mockTimelineResults } from '../../../../common/mock/timeline_results';
1617
import { OpenTimelineResult } from '../types';
1718
import { TimelinesTableProps } from '.';
@@ -233,4 +234,32 @@ describe('#getActionsColumns', () => {
233234

234235
expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]);
235236
});
237+
238+
test('it should not render "export timeline" if it is not included', () => {
239+
const testProps: TimelinesTableProps = {
240+
...getMockTimelinesTableProps(mockResults),
241+
actionTimelineToShow: ['createFrom', 'duplicate'],
242+
};
243+
const wrapper = mountWithIntl(
244+
<ThemeProvider theme={theme}>
245+
<TimelinesTable {...testProps} />
246+
</ThemeProvider>
247+
);
248+
249+
expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false);
250+
});
251+
252+
test('it should not render "delete timeline" if it is not included', () => {
253+
const testProps: TimelinesTableProps = {
254+
...getMockTimelinesTableProps(mockResults),
255+
actionTimelineToShow: ['createFrom', 'duplicate'],
256+
};
257+
const wrapper = mountWithIntl(
258+
<ThemeProvider theme={theme}>
259+
<TimelinesTable {...testProps} />
260+
</ThemeProvider>
261+
);
262+
263+
expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false);
264+
});
236265
});

x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns';
2424
import { getCommonColumns } from './common_columns';
2525
import { getExtendedColumns } from './extended_columns';
2626
import { getIconHeaderColumns } from './icon_header_columns';
27-
import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
27+
import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline';
2828

2929
// there are a number of type mismatches across this file
3030
const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -159,7 +159,8 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
159159
};
160160

161161
const selection = {
162-
selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null,
162+
selectable: (timelineResult: OpenTimelineResult) =>
163+
timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable,
163164
selectableMessage: (selectable: boolean) =>
164165
!selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined,
165166
onSelectionChange,

x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TimelineStatus,
1515
TemplateTimelineTypeLiteral,
1616
RowRendererId,
17+
TimelineStatusLiteralWithNull,
1718
} from '../../../../common/types/timeline';
1819

1920
/** The users who added a timeline to favorites */
@@ -174,6 +175,8 @@ export interface OpenTimelineProps {
174175
sortField: string;
175176
/** this affects timeline's behaviour like editable / duplicatible */
176177
timelineType: TimelineTypeLiteralWithNull;
178+
/* active or immutable */
179+
timelineStatus: TimelineStatusLiteralWithNull;
177180
/** when timelineType === template, templatetimelineFilter is a JSX.Element */
178181
templateTimelineFilter: JSX.Element[] | null;
179182
/** timeline / timeline template */

0 commit comments

Comments
 (0)