Skip to content

Commit

Permalink
[Security Solution] Disable bulk actions for immutable timeline templ…
Browse files Browse the repository at this point in the history
…ates (#73687) (#74067)

* disablebulk actions for immutable timeline templates

* make immutable timelines not selectable

* hide selected count if timeline status is immutable

Co-authored-by: Elastic Machine <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
angorayc and elasticmachine authored Aug 3, 2020
1 parent e58eaf1 commit 42f10e3
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
sortField={sortField}
templateTimelineFilter={templateTimelineFilter}
timelineType={timelineType}
timelineStatus={timelineStatus}
timelineFilter={timelineTabs}
title={title}
totalSearchResultsCount={totalCount}
Expand Down Expand Up @@ -356,6 +357,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
sortField={sortField}
templateTimelineFilter={templateTimelineFilter}
timelineType={timelineType}
timelineStatus={timelineStatus}
timelineFilter={timelineFilters}
title={title}
totalSearchResultsCount={totalCount}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { TimelinesTableProps } from './timelines_table';
import { mockTimelineResults } from '../../../common/mock/timeline_results';
import { OpenTimeline } from './open_timeline';
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants';
import { TimelineType } from '../../../../common/types/timeline';
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';

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

Expand Down Expand Up @@ -50,6 +50,7 @@ describe('OpenTimeline', () => {
sortField: DEFAULT_SORT_FIELD,
title,
timelineType: TimelineType.default,
timelineStatus: TimelineStatus.active,
templateTimelineFilter: [<div />],
totalSearchResultsCount: mockSearchResults.length,
});
Expand Down Expand Up @@ -263,4 +264,136 @@ describe('OpenTimeline', () => {
`Showing: ${mockResults.length} timelines with "How was your day?"`
);
});

test("it should render bulk actions if timelineStatus is active (selecting custom templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true);
});

test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
});

test("it should render selected count if timelineStatus is active (selecting custom templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.active,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true);
});

test("it should not render bulk actions if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.immutable,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(false);
});

test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.immutable,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
).toEqual(['createFrom', 'duplicate']);
});

test("it should not render selected count if timelineStatus is immutable (selecting Elastic templates' tab)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: TimelineStatus.immutable,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(false);
});

test("it should render bulk actions if timelineStatus is null (no template timelines' tab selected)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true);
});

test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(
wrapper.find('[data-test-subj="timelines-table"]').first().prop('actionTimelineToShow')
).toEqual(['createFrom', 'duplicate', 'export', 'selectable', 'delete']);
});

test("it should render selected count if timelineStatus is null (no template timelines' tab selected)", () => {
const defaultProps = {
...getDefaultTestProps(mockResults),
timelineStatus: null,
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimeline {...defaultProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="selected-count"]').exists()).toEqual(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { EuiPanel, EuiBasicTable } from '@elastic/eui';
import React, { useCallback, useMemo, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';

import { TimelineType } from '../../../../common/types/timeline';
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
import { ImportDataModal } from '../../../common/components/import_data_modal';
import {
UtilityBarGroup,
Expand Down Expand Up @@ -55,6 +55,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
setImportDataModalToggle,
sortField,
timelineType = TimelineType.default,
timelineStatus,
timelineFilter,
templateTimelineFilter,
totalSearchResultsCount,
Expand Down Expand Up @@ -140,19 +141,23 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
}, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]);

const actionTimelineToShow = useMemo<ActionTimelineToShow[]>(() => {
const timelineActions: ActionTimelineToShow[] = [
'createFrom',
'duplicate',
'export',
'selectable',
];
const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate'];

if (onDeleteSelected != null && deleteTimelines != null) {
if (timelineStatus !== TimelineStatus.immutable) {
timelineActions.push('export');
timelineActions.push('selectable');
}

if (
onDeleteSelected != null &&
deleteTimelines != null &&
timelineStatus !== TimelineStatus.immutable
) {
timelineActions.push('delete');
}

return timelineActions;
}, [onDeleteSelected, deleteTimelines]);
}, [onDeleteSelected, deleteTimelines, timelineStatus]);

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

Expand Down Expand Up @@ -206,20 +211,24 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
</>
</UtilityBarText>
</UtilityBarGroup>

<UtilityBarGroup>
<UtilityBarText>
{timelineType === TimelineType.template
? i18n.SELECTED_TEMPLATES(selectedItems.length)
: i18n.SELECTED_TIMELINES(selectedItems.length)}
</UtilityBarText>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={getBatchItemsPopoverContent}
>
{i18n.BATCH_ACTIONS}
</UtilityBarAction>
{timelineStatus !== TimelineStatus.immutable && (
<>
<UtilityBarText data-test-subj="selected-count">
{timelineType === TimelineType.template
? i18n.SELECTED_TEMPLATES(selectedItems.length)
: i18n.SELECTED_TIMELINES(selectedItems.length)}
</UtilityBarText>
<UtilityBarAction
iconSide="right"
iconType="arrowDown"
popoverContent={getBatchItemsPopoverContent}
data-test-subj="utility-bar-action"
>
{i18n.BATCH_ACTIONS}
</UtilityBarAction>
</>
)}
<UtilityBarAction iconSide="right" iconType="refresh" onClick={onRefreshBtnClick}>
{i18n.REFRESH}
</UtilityBarAction>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { TimelinesTableProps } from '../timelines_table';
import { mockTimelineResults } from '../../../../common/mock/timeline_results';
import { OpenTimelineModalBody } from './open_timeline_modal_body';
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
import { TimelineType } from '../../../../../common/types/timeline';
import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline';

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

Expand Down Expand Up @@ -48,6 +48,7 @@ describe('OpenTimelineModal', () => {
sortDirection: DEFAULT_SORT_DIRECTION,
sortField: DEFAULT_SORT_FIELD,
timelineType: TimelineType.default,
timelineStatus: TimelineStatus.active,
templateTimelineFilter: [<div />],
title,
totalSearchResultsCount: mockSearchResults.length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';
import { ThemeProvider } from 'styled-components';

import '../../../../common/mock/match_media';

import { mockTimelineResults } from '../../../../common/mock/timeline_results';
import { OpenTimelineResult } from '../types';
import { TimelinesTableProps } from '.';
Expand Down Expand Up @@ -233,4 +234,32 @@ describe('#getActionsColumns', () => {

expect(enableExportTimelineDownloader).toBeCalledWith(mockResults[0]);
});

test('it should not render "export timeline" if it is not included', () => {
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(mockResults),
actionTimelineToShow: ['createFrom', 'duplicate'],
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="export-timeline"]').exists()).toEqual(false);
});

test('it should not render "delete timeline" if it is not included', () => {
const testProps: TimelinesTableProps = {
...getMockTimelinesTableProps(mockResults),
actionTimelineToShow: ['createFrom', 'duplicate'],
};
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<TimelinesTable {...testProps} />
</ThemeProvider>
);

expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toEqual(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { getActionsColumns } from './actions_columns';
import { getCommonColumns } from './common_columns';
import { getExtendedColumns } from './extended_columns';
import { getIconHeaderColumns } from './icon_header_columns';
import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline';

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

const selection = {
selectable: (timelineResult: OpenTimelineResult) => timelineResult.savedObjectId != null,
selectable: (timelineResult: OpenTimelineResult) =>
timelineResult.savedObjectId != null && timelineResult.status !== TimelineStatus.immutable,
selectableMessage: (selectable: boolean) =>
!selectable ? i18n.MISSING_SAVED_OBJECT_ID : undefined,
onSelectionChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TimelineStatus,
TemplateTimelineTypeLiteral,
RowRendererId,
TimelineStatusLiteralWithNull,
} from '../../../../common/types/timeline';

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

0 comments on commit 42f10e3

Please sign in to comment.