Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Simplify bulk action execution #143019

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React from 'react';

import {
goToRuleEditPage,
executeRulesBulkAction,
performTrackableBulkAction,
bulkExportRules,
} from '../../../pages/detection_engine/rules/all/actions';
import { RuleActionsOverflow } from '.';
Expand All @@ -33,7 +33,7 @@ jest.mock('../../../../common/lib/kibana', () => {
});
jest.mock('../../../pages/detection_engine/rules/all/actions');

const executeRulesBulkActionMock = executeRulesBulkAction as jest.Mock;
const performTrackableBulkActionMock = performTrackableBulkAction as jest.Mock;
const bulkExportRulesMock = bulkExportRules as jest.Mock;

const flushPromises = () => new Promise(setImmediate);
Expand Down Expand Up @@ -194,9 +194,7 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click');
wrapper.update();
expect(executeRulesBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ action: 'duplicate' })
);
expect(performTrackableBulkAction).toHaveBeenCalledWith('duplicate', ['id']);
});

test('it calls duplicate action with the rule and rule.id when rules-details-duplicate-rule is clicked', () => {
Expand All @@ -208,16 +206,14 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click');
wrapper.update();
expect(executeRulesBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ action: 'duplicate', search: { ids: ['id'] } })
);
expect(performTrackableBulkAction).toHaveBeenCalledWith('duplicate', ['id']);
});
});

test('it navigates to edit page after the rule is duplicated', async () => {
const rule = mockRule('id');
const ruleDuplicate = mockRule('newRule');
executeRulesBulkActionMock.mockImplementation(() =>
performTrackableBulkActionMock.mockImplementation(() =>
Promise.resolve({ attributes: { results: { created: [ruleDuplicate] } } })
);
const wrapper = mount(
Expand All @@ -229,9 +225,7 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
await flushPromises();

expect(executeRulesBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ action: 'duplicate' })
);
expect(performTrackableBulkAction).toHaveBeenCalledWith('duplicate', ['id']);
expect(goToRuleEditPage).toHaveBeenCalledWith(ruleDuplicate.id, expect.anything());
});

Expand All @@ -251,12 +245,7 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
await flushPromises();

expect(bulkExportRulesMock).toHaveBeenCalledWith(
expect.objectContaining({ action: 'export' })
);
expect(bulkExportRulesMock).toHaveBeenCalledWith(
expect.not.objectContaining({ onSuccess: expect.any })
);
expect(bulkExportRulesMock).toHaveBeenCalledWith(['id'], undefined);
});
test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => {
const rule = mockRule('id');
Expand Down Expand Up @@ -353,9 +342,7 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click');
wrapper.update();
expect(executeRulesBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ action: 'delete' })
);
expect(performTrackableBulkAction).toHaveBeenCalledWith('delete', ['id']);
});

test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => {
Expand All @@ -367,9 +354,7 @@ describe('RuleActionsOverflow', () => {
wrapper.update();
wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click');
wrapper.update();
expect(executeRulesBulkAction).toHaveBeenCalledWith(
expect.objectContaining({ action: 'delete', search: { ids: ['id'] } })
);
expect(performTrackableBulkAction).toHaveBeenCalledWith('delete', ['id']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
EuiPopover,
EuiToolTip,
} from '@elastic/eui';
import { noop } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
import { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
Expand All @@ -26,9 +25,10 @@ import { useKibana } from '../../../../common/lib/kibana';
import { getToolTipContent } from '../../../../common/utils/privileges';
import type { Rule } from '../../../containers/detection_engine/rules';
import {
executeRulesBulkAction,
goToRuleEditPage,
bulkExportRules,
showBulkErrorToast,
performTrackableBulkAction,
} from '../../../pages/detection_engine/rules/all/actions';
import * as i18nActions from '../../../pages/detection_engine/rules/translations';
import * as i18n from './translations';
Expand Down Expand Up @@ -62,14 +62,6 @@ const RuleActionsOverflowComponent = ({
const { navigateToApp } = useKibana().services.application;
const toasts = useAppToasts();
const { startTransaction } = useStartTransaction();

const onRuleDeletedCallback = useCallback(() => {
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getRulesUrl(),
});
}, [navigateToApp]);

const actions = useMemo(
() =>
rule != null
Expand All @@ -82,15 +74,18 @@ const RuleActionsOverflowComponent = ({
onClick={async () => {
startTransaction({ name: SINGLE_RULE_ACTIONS.DUPLICATE });
closePopover();
const result = await executeRulesBulkAction({
action: BulkAction.duplicate,
onSuccess: noop,
search: { ids: [rule.id] },
toasts,
});
const createdRules = result?.attributes.results.created;
if (createdRules?.length) {
goToRuleEditPage(createdRules[0].id, navigateToApp);

try {
const response = await performTrackableBulkAction(BulkAction.duplicate, [
rule.id,
]);
const createdRules = response.attributes.results.created;

if (createdRules.length > 0) {
goToRuleEditPage(createdRules[0].id, navigateToApp);
}
} catch (e) {
showBulkErrorToast(toasts, BulkAction.duplicate, e);
}
}}
>
Expand All @@ -109,11 +104,7 @@ const RuleActionsOverflowComponent = ({
onClick={async () => {
startTransaction({ name: SINGLE_RULE_ACTIONS.EXPORT });
closePopover();
await bulkExportRules({
action: BulkAction.export,
search: { ids: [rule.id] },
toasts,
});
await bulkExportRules([rule.id], toasts);
}}
>
{i18nActions.EXPORT_RULE}
Expand All @@ -126,12 +117,17 @@ const RuleActionsOverflowComponent = ({
onClick={async () => {
startTransaction({ name: SINGLE_RULE_ACTIONS.DELETE });
closePopover();
await executeRulesBulkAction({
action: BulkAction.delete,
onSuccess: onRuleDeletedCallback,
search: { ids: [rule.id] },
toasts,
});

try {
await performTrackableBulkAction(BulkAction.delete, [rule.id]);

navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
path: getRulesUrl(),
});
} catch (e) {
showBulkErrorToast(toasts, BulkAction.delete, e);
}
}}
>
{i18nActions.DELETE_RULE}
Expand All @@ -142,7 +138,6 @@ const RuleActionsOverflowComponent = ({
canDuplicateRuleWithActions,
closePopover,
navigateToApp,
onRuleDeletedCallback,
rule,
startTransaction,
toasts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@

import type { EuiSwitchEvent } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui';
import { noop } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { useUpdateRulesCache } from '../../../containers/detection_engine/rules/use_find_rules_query';
import { executeRulesBulkAction } from '../../../pages/detection_engine/rules/all/actions';
import {
performTrackableBulkAction,
showBulkErrorToast,
showBulkSuccessToast,
} from '../../../pages/detection_engine/rules/all/actions';
import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context';

const StaticSwitch = styled(EuiSwitch)`
Expand Down Expand Up @@ -57,19 +60,28 @@ export const RuleSwitchComponent = ({
startTransaction({
name: enabled ? SINGLE_RULE_ACTIONS.DISABLE : SINGLE_RULE_ACTIONS.ENABLE,
});
const bulkActionResponse = await executeRulesBulkAction({
setLoadingRules: rulesTableContext?.actions.setLoadingRules,
toasts,
onSuccess: rulesTableContext ? undefined : noop,
action: event.target.checked ? BulkAction.enable : BulkAction.disable,
search: { ids: [id] },
visibleRuleIds: [],
});
if (bulkActionResponse?.attributes.results.updated.length) {
// The rule was successfully updated
updateRulesCache(bulkActionResponse.attributes.results.updated);
onChange?.(bulkActionResponse.attributes.results.updated[0].enabled);

const action = event.target.checked ? BulkAction.enable : BulkAction.disable;

rulesTableContext?.actions.setLoadingRules({ ids: [], action });

try {
const response = await performTrackableBulkAction(action, [id]);

if (rulesTableContext) {
showBulkSuccessToast(toasts, action, response.attributes.summary);
}

if (response.attributes.results.updated.length > 0) {
// The rule was successfully updated
updateRulesCache(response.attributes.results.updated);
onChange?.(response.attributes.results.updated[0].enabled);
}
} catch (e) {
showBulkErrorToast(toasts, action, e);
}

rulesTableContext?.actions.setLoadingRules({ ids: [], action: null });
setMyIsLoading(false);
},
[enabled, id, onChange, rulesTableContext, startTransaction, toasts, updateRulesCache]
Expand Down
Loading