diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index fa663e5f487ca..1e621363f5f29 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -191,7 +191,7 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise { history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); }; -export const runRuleAction = () => {}; - export const duplicateRuleAction = async ( rule: Rule, dispatch: React.Dispatch, @@ -37,6 +39,7 @@ export const duplicateRuleAction = async ( const duplicatedRule = await duplicateRules({ rules: [rule] }); dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id }); + displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster); } catch (e) { displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); } @@ -49,7 +52,8 @@ export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch< export const deleteRulesAction = async ( ids: string[], dispatch: React.Dispatch, - dispatchToaster: Dispatch + dispatchToaster: Dispatch, + onRuleDeleted?: () => void ) => { try { dispatch({ type: 'updateLoading', ids, isLoading: true }); @@ -65,6 +69,9 @@ export const deleteRulesAction = async ( errors.map(e => e.error.message), dispatchToaster ); + } else { + // FP: See https://github.com/typescript-eslint/typescript-eslint/issues/1138#issuecomment-566929566 + onRuleDeleted?.(); // eslint-disable-line no-unused-expressions } } catch (e) { displayErrorToast( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index 0971ef0149304..06d4c709a32bf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -6,22 +6,26 @@ import { EuiContextMenuItem } from '@elastic/eui'; import React, { Dispatch } from 'react'; +import * as H from 'history'; import * as i18n from '../translations'; import { TableData } from '../types'; import { Action } from './reducer'; import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; import { ActionToaster } from '../../../../components/toasters'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; export const getBatchItems = ( selectedState: TableData[], dispatch: Dispatch, dispatchToaster: Dispatch, + history: H.History, closePopover: () => void ) => { const containsEnabled = selectedState.some(v => v.activate); const containsDisabled = selectedState.some(v => !v.activate); const containsLoading = selectedState.some(v => v.isLoading); const containsImmutable = selectedState.some(v => v.immutable); + const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1; return [ { closePopover(); + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${selectedState[0].id}/edit`); }} > {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index ed5dc6913151a..877c32ecf02ec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -22,7 +22,6 @@ import { duplicateRuleAction, editRuleAction, exportRulesAction, - runRuleAction, } from './actions'; import { Action } from './reducer'; @@ -45,13 +44,6 @@ const getActions = ( onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history), enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable, }, - { - description: i18n.RUN_RULE_MANUALLY, - icon: 'play', - name: i18n.RUN_RULE_MANUALLY, - onClick: runRuleAction, - enabled: () => false, - }, { description: i18n.DUPLICATE_RULE, icon: 'copy', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index cb4ffa127781d..4aa6b778582f9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -85,10 +85,10 @@ export const AllRules = React.memo<{ const getBatchItemsPopoverContent = useCallback( (closePopover: () => void) => ( ), - [selectedItems, dispatch, dispatchToaster] + [selectedItems, dispatch, dispatchToaster, history] ); const tableOnChangeCallback = useCallback( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..b981720d4fac0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="ruleActionsOverflow" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + Duplicate rule… + , + + Export rule + , + + Delete rule… + , + ] + } + /> + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx new file mode 100644 index 0000000000000..47df07d8c51f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; + +import { RuleActionsOverflow } from './index'; +import { mockRule } from '../../all/__mocks__/mock'; + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), +})); + +describe('RuleActionsOverflow', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx new file mode 100644 index 0000000000000..0a823ce545d72 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiToolTip, +} from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { noop } from 'lodash/fp'; +import { useHistory } from 'react-router-dom'; +import { Rule } from '../../../../../containers/detection_engine/rules'; +import * as i18n from './translations'; +import * as i18nActions from '../../../rules/translations'; +import { deleteRulesAction, duplicateRuleAction } from '../../all/actions'; +import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters'; +import { RuleDownloader } from '../rule_downloader'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine'; + +interface RuleActionsOverflowComponentProps { + rule: Rule | null; + userHasNoPermissions: boolean; +} + +/** + * Overflow Actions for a Rule + */ +const RuleActionsOverflowComponent = ({ + rule, + userHasNoPermissions, +}: RuleActionsOverflowComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [rulesToExport, setRulesToExport] = useState(undefined); + const history = useHistory(); + const [, dispatchToaster] = useStateToaster(); + + const onRuleDeletedCallback = useCallback(() => { + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules`); + }, [history]); + + const actions = useMemo( + () => + rule != null + ? [ + { + setIsPopoverOpen(false); + await duplicateRuleAction(rule, noop, dispatchToaster); + }} + > + {i18nActions.DUPLICATE_RULE} + , + { + setIsPopoverOpen(false); + setRulesToExport([rule]); + }} + > + {i18nActions.EXPORT_RULE} + , + { + setIsPopoverOpen(false); + await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); + }} + > + {i18nActions.DELETE_RULE} + , + ] + : [], + [rule, userHasNoPermissions] + ); + + return ( + <> + + setIsPopoverOpen(!isPopoverOpen)} + /> + + } + closePopover={() => setIsPopoverOpen(false)} + id="ruleActionsOverflow" + isOpen={isPopoverOpen} + ownFocus={true} + panelPaddingSize="none" + > + + + { + displaySuccessToast( + i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), + dispatchToaster + ); + }} + /> + + ); +}; + +export const RuleActionsOverflow = React.memo(RuleActionsOverflowComponent); + +RuleActionsOverflow.displayName = 'RuleActionsOverflow'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts new file mode 100644 index 0000000000000..631fbe41870d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts @@ -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 ALL_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.rules.components.ruleActionsOverflow.allActionsTitle', + { + defaultMessage: 'All actions', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 8839185f11427..099006a34920c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -63,6 +63,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { getEmptyTagValue } from '../../../../components/empty_value'; import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; +import { RuleActionsOverflow } from '../components/rule_actions_overflow'; interface ReduxProps { filters: esFilters.Filter[]; @@ -302,6 +303,12 @@ const RuleDetailsComponent = memo( {ruleI18n.EDIT_RULE_SETTINGS} + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 1e47d1a57facc..596d1d9b3e7d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -21,13 +21,6 @@ export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.add defaultMessage: 'Add new rule', }); -export const ACTIVITY_MONITOR = i18n.translate( - 'xpack.siem.detectionEngine.rules.activityMonitorTitle', - { - defaultMessage: 'Activity monitor', - } -); - export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { defaultMessage: 'Rules', }); @@ -163,10 +156,10 @@ export const EDIT_RULE_SETTINGS = i18n.translate( } ); -export const RUN_RULE_MANUALLY = i18n.translate( - 'xpack.siem.detectionEngine.rules.allRules.actions.runRuleManuallyDescription', +export const DUPLICATE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.duplicateTitle', { - defaultMessage: 'Run rule manually…', + defaultMessage: 'Duplicate', } ); @@ -177,6 +170,13 @@ export const DUPLICATE_RULE = i18n.translate( } ); +export const SUCCESSFULLY_DUPLICATED_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle', { + values: { totalRules }, + defaultMessage: + 'Successfully duplicated {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + }); + export const DUPLICATE_RULE_ERROR = i18n.translate( 'xpack.siem.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription', {