Skip to content

Commit c076499

Browse files
[SIEM][Detection Engine] Adds actions to Rule Details (#54828) (#54868)
This PR adds the following actions to the `Rule Details` page via the `RuleActionsOverflow` component (which is permission-aware): * Duplicate * Export * Delete Additional fixes include: * Fixes duplication action (recent regression as part of status update additions) * i18n of `Duplicate` postfix when duplicating rules * Adds success toast when duplication is a success * Enabled `Edit Index Patterns` batch action * Removes unused `Run Rule Manually` action Rule Details Actions: ![image](https://user-images.githubusercontent.com/2946766/72385375-9c3a6880-36dc-11ea-8249-4ae92eb72dd1.png) Edit Index Patterns Batch Action: ![image](https://user-images.githubusercontent.com/2946766/72385468-c5f38f80-36dc-11ea-93c8-b70e4982f01a.png) Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [X] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ Co-authored-by: Elastic Machine <[email protected]>
1 parent e0d6407 commit c076499

File tree

11 files changed

+275
-29
lines changed

11 files changed

+275
-29
lines changed

x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru
191191
},
192192
body: JSON.stringify({
193193
...rule,
194-
name: `${rule.name} [Duplicate]`,
194+
name: `${rule.name} [${i18n.DUPLICATE}]`,
195195
created_at: undefined,
196196
created_by: undefined,
197197
id: undefined,
@@ -200,6 +200,10 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru
200200
updated_by: undefined,
201201
enabled: rule.enabled,
202202
immutable: false,
203+
last_success_at: undefined,
204+
last_success_message: undefined,
205+
status: undefined,
206+
status_date: undefined,
203207
}),
204208
})
205209
);

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import {
1616
} from '../../../../containers/detection_engine/rules';
1717
import { Action } from './reducer';
1818

19-
import { ActionToaster, displayErrorToast } from '../../../../components/toasters';
19+
import {
20+
ActionToaster,
21+
displayErrorToast,
22+
displaySuccessToast,
23+
} from '../../../../components/toasters';
2024

2125
import * as i18n from '../translations';
2226
import { bucketRulesResponse } from './helpers';
@@ -25,8 +29,6 @@ export const editRuleAction = (rule: Rule, history: H.History) => {
2529
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`);
2630
};
2731

28-
export const runRuleAction = () => {};
29-
3032
export const duplicateRuleAction = async (
3133
rule: Rule,
3234
dispatch: React.Dispatch<Action>,
@@ -37,6 +39,7 @@ export const duplicateRuleAction = async (
3739
const duplicatedRule = await duplicateRules({ rules: [rule] });
3840
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false });
3941
dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id });
42+
displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster);
4043
} catch (e) {
4144
displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster);
4245
}
@@ -49,7 +52,8 @@ export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch<
4952
export const deleteRulesAction = async (
5053
ids: string[],
5154
dispatch: React.Dispatch<Action>,
52-
dispatchToaster: Dispatch<ActionToaster>
55+
dispatchToaster: Dispatch<ActionToaster>,
56+
onRuleDeleted?: () => void
5357
) => {
5458
try {
5559
dispatch({ type: 'updateLoading', ids, isLoading: true });
@@ -65,6 +69,9 @@ export const deleteRulesAction = async (
6569
errors.map(e => e.error.message),
6670
dispatchToaster
6771
);
72+
} else {
73+
// FP: See https://github.com/typescript-eslint/typescript-eslint/issues/1138#issuecomment-566929566
74+
onRuleDeleted?.(); // eslint-disable-line no-unused-expressions
6875
}
6976
} catch (e) {
7077
displayErrorToast(

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66

77
import { EuiContextMenuItem } from '@elastic/eui';
88
import React, { Dispatch } from 'react';
9+
import * as H from 'history';
910
import * as i18n from '../translations';
1011
import { TableData } from '../types';
1112
import { Action } from './reducer';
1213
import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions';
1314
import { ActionToaster } from '../../../../components/toasters';
15+
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
1416

1517
export const getBatchItems = (
1618
selectedState: TableData[],
1719
dispatch: Dispatch<Action>,
1820
dispatchToaster: Dispatch<ActionToaster>,
21+
history: H.History,
1922
closePopover: () => void
2023
) => {
2124
const containsEnabled = selectedState.some(v => v.activate);
2225
const containsDisabled = selectedState.some(v => !v.activate);
2326
const containsLoading = selectedState.some(v => v.isLoading);
2427
const containsImmutable = selectedState.some(v => v.immutable);
28+
const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1;
2529

2630
return [
2731
<EuiContextMenuItem
@@ -65,9 +69,12 @@ export const getBatchItems = (
6569
<EuiContextMenuItem
6670
key={i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS}
6771
icon="indexEdit"
68-
disabled={true}
72+
disabled={
73+
containsImmutable || containsLoading || containsMultipleRules || selectedState.length === 0
74+
}
6975
onClick={async () => {
7076
closePopover();
77+
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${selectedState[0].id}/edit`);
7178
}}
7279
>
7380
{i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS}

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
/* eslint-disable react/display-name */
88

99
import {
10-
EuiTableActionsColumnType,
11-
EuiBasicTableColumn,
1210
EuiBadge,
1311
EuiIconTip,
1412
EuiLink,
1513
EuiTextColor,
14+
EuiBasicTableColumn,
15+
EuiTableActionsColumnType,
1616
} from '@elastic/eui';
1717
import * as H from 'history';
1818
import React, { Dispatch } from 'react';
@@ -22,7 +22,6 @@ import {
2222
duplicateRuleAction,
2323
editRuleAction,
2424
exportRulesAction,
25-
runRuleAction,
2625
} from './actions';
2726

2827
import { Action } from './reducer';
@@ -46,14 +45,6 @@ const getActions = (
4645
onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history),
4746
enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable,
4847
},
49-
{
50-
description: i18n.RUN_RULE_MANUALLY,
51-
type: 'icon',
52-
icon: 'play',
53-
name: i18n.RUN_RULE_MANUALLY,
54-
onClick: runRuleAction,
55-
enabled: () => false,
56-
},
5748
{
5849
description: i18n.DUPLICATE_RULE,
5950
type: 'icon',

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ export const AllRules = React.memo<{
8585
const getBatchItemsPopoverContent = useCallback(
8686
(closePopover: () => void) => (
8787
<EuiContextMenuPanel
88-
items={getBatchItems(selectedItems, dispatch, dispatchToaster, closePopover)}
88+
items={getBatchItems(selectedItems, dispatch, dispatchToaster, history, closePopover)}
8989
/>
9090
),
91-
[selectedItems, dispatch, dispatchToaster]
91+
[selectedItems, dispatch, dispatchToaster, history]
9292
);
9393

9494
const tableOnChangeCallback = useCallback(

x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { shallow } from 'enzyme';
8+
import React from 'react';
9+
10+
import { RuleActionsOverflow } from './index';
11+
import { mockRule } from '../../all/__mocks__/mock';
12+
13+
jest.mock('react-router-dom', () => ({
14+
useHistory: () => ({
15+
push: jest.fn(),
16+
}),
17+
}));
18+
19+
describe('RuleActionsOverflow', () => {
20+
test('renders correctly against snapshot', () => {
21+
const wrapper = shallow(
22+
<RuleActionsOverflow rule={mockRule('id')} userHasNoPermissions={false} />
23+
);
24+
expect(wrapper).toMatchSnapshot();
25+
});
26+
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
EuiButtonIcon,
9+
EuiContextMenuItem,
10+
EuiContextMenuPanel,
11+
EuiPopover,
12+
EuiToolTip,
13+
} from '@elastic/eui';
14+
import React, { useCallback, useMemo, useState } from 'react';
15+
16+
import { noop } from 'lodash/fp';
17+
import { useHistory } from 'react-router-dom';
18+
import { Rule } from '../../../../../containers/detection_engine/rules';
19+
import * as i18n from './translations';
20+
import * as i18nActions from '../../../rules/translations';
21+
import { deleteRulesAction, duplicateRuleAction } from '../../all/actions';
22+
import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters';
23+
import { RuleDownloader } from '../rule_downloader';
24+
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
25+
26+
interface RuleActionsOverflowComponentProps {
27+
rule: Rule | null;
28+
userHasNoPermissions: boolean;
29+
}
30+
31+
/**
32+
* Overflow Actions for a Rule
33+
*/
34+
const RuleActionsOverflowComponent = ({
35+
rule,
36+
userHasNoPermissions,
37+
}: RuleActionsOverflowComponentProps) => {
38+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
39+
const [rulesToExport, setRulesToExport] = useState<Rule[] | undefined>(undefined);
40+
const history = useHistory();
41+
const [, dispatchToaster] = useStateToaster();
42+
43+
const onRuleDeletedCallback = useCallback(() => {
44+
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules`);
45+
}, [history]);
46+
47+
const actions = useMemo(
48+
() =>
49+
rule != null
50+
? [
51+
<EuiContextMenuItem
52+
key={i18nActions.DUPLICATE_RULE}
53+
icon="exportAction"
54+
disabled={userHasNoPermissions}
55+
onClick={async () => {
56+
setIsPopoverOpen(false);
57+
await duplicateRuleAction(rule, noop, dispatchToaster);
58+
}}
59+
>
60+
{i18nActions.DUPLICATE_RULE}
61+
</EuiContextMenuItem>,
62+
<EuiContextMenuItem
63+
key={i18nActions.EXPORT_RULE}
64+
icon="indexEdit"
65+
disabled={userHasNoPermissions || rule.immutable}
66+
onClick={async () => {
67+
setIsPopoverOpen(false);
68+
setRulesToExport([rule]);
69+
}}
70+
>
71+
{i18nActions.EXPORT_RULE}
72+
</EuiContextMenuItem>,
73+
<EuiContextMenuItem
74+
key={i18nActions.DELETE_RULE}
75+
icon="trash"
76+
disabled={userHasNoPermissions || rule.immutable}
77+
onClick={async () => {
78+
setIsPopoverOpen(false);
79+
await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback);
80+
}}
81+
>
82+
{i18nActions.DELETE_RULE}
83+
</EuiContextMenuItem>,
84+
]
85+
: [],
86+
[rule, userHasNoPermissions]
87+
);
88+
89+
return (
90+
<>
91+
<EuiPopover
92+
anchorPosition="leftCenter"
93+
button={
94+
<EuiToolTip position="top" content={i18n.ALL_ACTIONS}>
95+
<EuiButtonIcon
96+
iconType="boxesHorizontal"
97+
aria-label={i18n.ALL_ACTIONS}
98+
isDisabled={userHasNoPermissions}
99+
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
100+
/>
101+
</EuiToolTip>
102+
}
103+
closePopover={() => setIsPopoverOpen(false)}
104+
id="ruleActionsOverflow"
105+
isOpen={isPopoverOpen}
106+
ownFocus={true}
107+
panelPaddingSize="none"
108+
>
109+
<EuiContextMenuPanel items={actions} />
110+
</EuiPopover>
111+
<RuleDownloader
112+
filename={`${i18nActions.EXPORT_FILENAME}.ndjson`}
113+
rules={rulesToExport}
114+
onExportComplete={exportCount => {
115+
displaySuccessToast(
116+
i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount),
117+
dispatchToaster
118+
);
119+
}}
120+
/>
121+
</>
122+
);
123+
};
124+
125+
export const RuleActionsOverflow = React.memo(RuleActionsOverflowComponent);
126+
127+
RuleActionsOverflow.displayName = 'RuleActionsOverflow';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
9+
export const ALL_ACTIONS = i18n.translate(
10+
'xpack.siem.detectionEngine.rules.components.ruleActionsOverflow.allActionsTitle',
11+
{
12+
defaultMessage: 'All actions',
13+
}
14+
);

0 commit comments

Comments
 (0)