mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Ram] create bulk delete on rules front (#144101)
issue: https://github.com/elastic/kibana/issues/143826 ## Summary In this PR I am enabling `Delete` in menu when Select all is chosen. And trigger new bulk delete API when `Delete` option will be chosen. <img width="453" alt="Screenshot 2022-10-23 at 16 34 09" src="https://user-images.githubusercontent.com/26089545/198290071-e7d6be54-286c-4a7c-a579-1d07ac23d3db.png"> ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
9fda59f512
commit
2b2e1d19d2
14 changed files with 732 additions and 142 deletions
|
@ -6,10 +6,16 @@
|
|||
*/
|
||||
|
||||
import { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import {
|
||||
getSuccessfulDeletionNotificationText,
|
||||
getFailedDeletionNotificationText,
|
||||
getConfirmDeletionButtonText,
|
||||
getConfirmDeletionModalText,
|
||||
CANCEL_BUTTON_TEXT,
|
||||
} from '../sections/rules_list/translations';
|
||||
|
||||
export const DeleteModalConfirmation = ({
|
||||
idsToDelete,
|
||||
|
@ -54,33 +60,12 @@ export const DeleteModalConfirmation = ({
|
|||
if (!deleteModalFlyoutVisible) {
|
||||
return null;
|
||||
}
|
||||
const confirmModalText = i18n.translate(
|
||||
'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
"You won't be able to recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.",
|
||||
values: { numIdsToDelete, singleTitle, multipleTitle },
|
||||
}
|
||||
);
|
||||
const confirmButtonText = i18n.translate(
|
||||
'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ',
|
||||
values: { numIdsToDelete, singleTitle, multipleTitle },
|
||||
}
|
||||
);
|
||||
const cancelButtonText = i18n.translate(
|
||||
'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="deleteIdsConfirmation"
|
||||
title={confirmButtonText}
|
||||
title={getConfirmDeletionButtonText(numIdsToDelete, singleTitle, multipleTitle)}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisibility(false);
|
||||
onCancel();
|
||||
|
@ -95,36 +80,22 @@ export const DeleteModalConfirmation = ({
|
|||
const numErrors = errors.length;
|
||||
if (numSuccesses > 0) {
|
||||
toasts.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}',
|
||||
values: { numSuccesses, singleTitle, multipleTitle },
|
||||
}
|
||||
)
|
||||
getSuccessfulDeletionNotificationText(numSuccesses, singleTitle, multipleTitle)
|
||||
);
|
||||
}
|
||||
|
||||
if (numErrors > 0) {
|
||||
toasts.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}',
|
||||
values: { numErrors, singleTitle, multipleTitle },
|
||||
}
|
||||
)
|
||||
getFailedDeletionNotificationText(numErrors, singleTitle, multipleTitle)
|
||||
);
|
||||
await onErrors();
|
||||
}
|
||||
await onDeleted(successes);
|
||||
}}
|
||||
cancelButtonText={cancelButtonText}
|
||||
confirmButtonText={confirmButtonText}
|
||||
cancelButtonText={CANCEL_BUTTON_TEXT}
|
||||
confirmButtonText={getConfirmDeletionButtonText(numIdsToDelete, singleTitle, multipleTitle)}
|
||||
>
|
||||
<p>{confirmModalText}</p>
|
||||
<p>{getConfirmDeletionModalText(numIdsToDelete, singleTitle, multipleTitle)}</p>
|
||||
{showWarningText && (
|
||||
<EuiCallOut title={<>{warningText}</>} color="warning" iconType="alert" />
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { CANCEL_BUTTON_TEXT } from '../sections/rules_list/translations';
|
||||
|
||||
export const RulesDeleteModalConfirmation = ({
|
||||
confirmButtonText,
|
||||
confirmModalText,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
showWarningText,
|
||||
warningText,
|
||||
}: {
|
||||
confirmButtonText: string;
|
||||
confirmModalText: string;
|
||||
onConfirm: () => Promise<void>;
|
||||
onCancel: () => void;
|
||||
showWarningText?: boolean;
|
||||
warningText?: string;
|
||||
}) => (
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="rulesDeleteConfirmation"
|
||||
title={confirmButtonText}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText={CANCEL_BUTTON_TEXT}
|
||||
confirmButtonText={confirmButtonText}
|
||||
>
|
||||
<p>{confirmModalText}</p>
|
||||
{showWarningText && <EuiCallOut title={<>{warningText}</>} color="warning" iconType="alert" />}
|
||||
</EuiConfirmModal>
|
||||
);
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { BulkDeleteResponse } from '../../types';
|
||||
import {
|
||||
getSuccessfulDeletionNotificationText,
|
||||
getFailedDeletionNotificationText,
|
||||
getPartialSuccessDeletionNotificationText,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE,
|
||||
} from '../sections/rules_list/translations';
|
||||
|
||||
export const useBulkDeleteResponse = ({
|
||||
onSearchPopulate,
|
||||
}: {
|
||||
onSearchPopulate?: (filter: string) => void;
|
||||
}) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const onSearchPopulateInternal = useCallback(
|
||||
(response: BulkDeleteResponse) => {
|
||||
if (!onSearchPopulate) {
|
||||
return;
|
||||
}
|
||||
const filter = response.errors.map((error) => error.rule.name).join(',');
|
||||
onSearchPopulate(filter);
|
||||
},
|
||||
[onSearchPopulate]
|
||||
);
|
||||
|
||||
const renderToastErrorBody = useCallback(
|
||||
(response: BulkDeleteResponse, messageType: 'warning' | 'danger') => {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
|
||||
{onSearchPopulate && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color={messageType}
|
||||
size="s"
|
||||
onClick={() => onSearchPopulateInternal(response)}
|
||||
data-test-subj="bulkDeleteResponseFilterErrors"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.filterByErrors"
|
||||
defaultMessage="Filter by errored rules"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
[onSearchPopulate, onSearchPopulateInternal]
|
||||
);
|
||||
|
||||
const showToast = useCallback(
|
||||
(response: BulkDeleteResponse) => {
|
||||
const { errors, total } = response;
|
||||
|
||||
const numberOfSuccess = total - errors.length;
|
||||
const numberOfErrors = errors.length;
|
||||
|
||||
// All success
|
||||
if (!numberOfErrors) {
|
||||
toasts.addSuccess(
|
||||
getSuccessfulDeletionNotificationText(
|
||||
numberOfSuccess,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All failure
|
||||
if (numberOfErrors === total) {
|
||||
toasts.addDanger({
|
||||
title: getFailedDeletionNotificationText(
|
||||
numberOfErrors,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE
|
||||
),
|
||||
text: toMountPoint(renderToastErrorBody(response, 'danger')),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Some failure
|
||||
toasts.addWarning({
|
||||
title: getPartialSuccessDeletionNotificationText(
|
||||
numberOfSuccess,
|
||||
numberOfErrors,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE
|
||||
),
|
||||
text: toMountPoint(renderToastErrorBody(response, 'warning')),
|
||||
});
|
||||
},
|
||||
[toasts, renderToastErrorBody]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
showToast,
|
||||
};
|
||||
}, [showToast]);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
import { BulkDeleteResponse } from '../../../types';
|
||||
|
||||
export const bulkDeleteRules = async ({
|
||||
filter,
|
||||
ids,
|
||||
http,
|
||||
}: {
|
||||
filter?: KueryNode | null;
|
||||
ids: string[];
|
||||
http: HttpSetup;
|
||||
}): Promise<BulkDeleteResponse> => {
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
ids: ids?.length ? ids : undefined,
|
||||
...(filter ? { filter: JSON.stringify(filter) } : {}),
|
||||
});
|
||||
|
||||
return http.patch(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, { body });
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to parse bulk delete params: ${e}`);
|
||||
}
|
||||
};
|
|
@ -45,3 +45,4 @@ export { unsnoozeRule, bulkUnsnoozeRules } from './unsnooze';
|
|||
export type { BulkUpdateAPIKeyProps } from './update_api_key';
|
||||
export { updateAPIKey, bulkUpdateAPIKey } from './update_api_key';
|
||||
export { runSoon } from './run_soon';
|
||||
export { bulkDeleteRules } from './bulk_delete';
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
onPerformingAction={() => {}}
|
||||
onActionPerformed={() => {}}
|
||||
setRulesToDelete={() => {}}
|
||||
setRulesToDeleteFilter={() => {}}
|
||||
setRulesToUpdateAPIKey={() => {}}
|
||||
setRulesToSnooze={() => {}}
|
||||
setRulesToUnsnooze={() => {}}
|
||||
|
@ -64,7 +65,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
expect(wrapper.find('[data-test-subj="enableAll"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="disableAll"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="updateAPIKeys"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="deleteAll"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkDelete"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkSnooze"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkUnsnooze"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').exists()).toBeTruthy();
|
||||
|
@ -85,6 +86,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
onPerformingAction={() => {}}
|
||||
onActionPerformed={() => {}}
|
||||
setRulesToDelete={() => {}}
|
||||
setRulesToDeleteFilter={() => {}}
|
||||
setRulesToUpdateAPIKey={() => {}}
|
||||
setRulesToSnooze={() => {}}
|
||||
setRulesToUnsnooze={() => {}}
|
||||
|
@ -116,6 +118,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
onPerformingAction={() => {}}
|
||||
onActionPerformed={() => {}}
|
||||
setRulesToDelete={() => {}}
|
||||
setRulesToDeleteFilter={() => {}}
|
||||
setRulesToUpdateAPIKey={() => {}}
|
||||
setRulesToSnooze={() => {}}
|
||||
setRulesToUnsnooze={() => {}}
|
||||
|
@ -130,7 +133,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="disableAll"]').first().prop('isDisabled')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="deleteAll"]').first().prop('isDisabled')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkDelete"]').first().prop('isDisabled')).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="updateAPIKeys"]').first().prop('isDisabled')).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="bulkSnooze"]').first().prop('isDisabled')).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="bulkUnsnooze"]').first().prop('isDisabled')).toBeFalsy();
|
||||
|
@ -157,6 +160,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
onPerformingAction={() => {}}
|
||||
onActionPerformed={() => {}}
|
||||
setRulesToDelete={() => {}}
|
||||
setRulesToDeleteFilter={() => {}}
|
||||
setRulesToSnooze={setRulesToSnooze}
|
||||
setRulesToUnsnooze={setRulesToUnsnooze}
|
||||
setRulesToSchedule={setRulesToSchedule}
|
||||
|
@ -207,6 +211,7 @@ describe('rule_quick_edit_buttons', () => {
|
|||
onPerformingAction={() => {}}
|
||||
onActionPerformed={() => {}}
|
||||
setRulesToDelete={() => {}}
|
||||
setRulesToDeleteFilter={() => {}}
|
||||
setRulesToSnooze={setRulesToSnooze}
|
||||
setRulesToUnsnooze={setRulesToUnsnooze}
|
||||
setRulesToSchedule={setRulesToSchedule}
|
||||
|
|
|
@ -25,12 +25,14 @@ export type ComponentOpts = {
|
|||
getFilter: () => KueryNode | null;
|
||||
onPerformingAction?: () => void;
|
||||
onActionPerformed?: () => void;
|
||||
isDeletingRules?: boolean;
|
||||
isSnoozingRules?: boolean;
|
||||
isUnsnoozingRules?: boolean;
|
||||
isSchedulingRules?: boolean;
|
||||
isUnschedulingRules?: boolean;
|
||||
isUpdatingRuleAPIKeys?: boolean;
|
||||
setRulesToDelete: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setRulesToDeleteFilter: React.Dispatch<React.SetStateAction<KueryNode | null | undefined>>;
|
||||
setRulesToUpdateAPIKey: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setRulesToSnooze: React.Dispatch<React.SetStateAction<RuleTableItem[]>>;
|
||||
setRulesToUnsnooze: React.Dispatch<React.SetStateAction<RuleTableItem[]>>;
|
||||
|
@ -71,6 +73,7 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
getFilter,
|
||||
onPerformingAction = noop,
|
||||
onActionPerformed = noop,
|
||||
isDeletingRules = false,
|
||||
isSnoozingRules = false,
|
||||
isUnsnoozingRules = false,
|
||||
isSchedulingRules = false,
|
||||
|
@ -79,6 +82,7 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
enableRules,
|
||||
disableRules,
|
||||
setRulesToDelete,
|
||||
setRulesToDeleteFilter,
|
||||
setRulesToUpdateAPIKey,
|
||||
setRulesToSnooze,
|
||||
setRulesToUnsnooze,
|
||||
|
@ -96,7 +100,6 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
|
||||
const [isEnablingRules, setIsEnablingRules] = useState<boolean>(false);
|
||||
const [isDisablingRules, setIsDisablingRules] = useState<boolean>(false);
|
||||
const [isDeletingRules, setIsDeletingRules] = useState<boolean>(false);
|
||||
|
||||
const isPerformingAction =
|
||||
isEnablingRules ||
|
||||
|
@ -169,13 +172,13 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
}
|
||||
|
||||
async function deleteSelectedItems() {
|
||||
if (isAllSelected) {
|
||||
return;
|
||||
}
|
||||
onPerformingAction();
|
||||
setIsDeletingRules(true);
|
||||
try {
|
||||
setRulesToDelete(selectedItems.map((selected: any) => selected.id));
|
||||
if (isAllSelected) {
|
||||
setRulesToDeleteFilter(getFilter());
|
||||
} else {
|
||||
setRulesToDelete(selectedItems.map((selected) => selected.id));
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate(
|
||||
|
@ -186,7 +189,6 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
),
|
||||
});
|
||||
} finally {
|
||||
setIsDeletingRules(false);
|
||||
onActionPerformed();
|
||||
}
|
||||
}
|
||||
|
@ -416,30 +418,20 @@ export const RuleQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ButtonWithTooltip
|
||||
showTooltip={isAllSelected}
|
||||
tooltip={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.deleteUnsupported',
|
||||
{
|
||||
defaultMessage: 'Bulk delete is unsupported when selecting all rules.',
|
||||
}
|
||||
)}
|
||||
<EuiButtonEmpty
|
||||
onClick={deleteSelectedItems}
|
||||
isLoading={isDeletingRules}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
isDisabled={isPerformingAction || hasDisabledByLicenseRuleTypes}
|
||||
data-test-subj="bulkDelete"
|
||||
className="actBulkActionPopover__deleteAll"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
onClick={deleteSelectedItems}
|
||||
isLoading={isDeletingRules}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
isDisabled={isPerformingAction || isAllSelected}
|
||||
data-test-subj="deleteAll"
|
||||
className="actBulkActionPopover__deleteAll"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.deleteAllTitle"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</ButtonWithTooltip>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.deleteAllTitle"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ jest.mock('../../../lib/rule_api', () => ({
|
|||
updateAPIKey: jest.fn(),
|
||||
loadRuleTags: jest.fn(),
|
||||
bulkSnoozeRules: jest.fn(),
|
||||
bulkDeleteRules: jest.fn().mockResolvedValue({ errors: [], total: 10 }),
|
||||
bulkUnsnoozeRules: jest.fn(),
|
||||
bulkUpdateAPIKey: jest.fn(),
|
||||
alertingFrameworkHealth: jest.fn(() => ({
|
||||
|
@ -102,6 +103,10 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
// This entire test suite is flaky/timing out and has been skipped.
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/134922
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/134923
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/134924
|
||||
|
|
|
@ -64,13 +64,12 @@ import {
|
|||
enableRule,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
deleteRules,
|
||||
bulkUpdateAPIKey,
|
||||
} from '../../../lib/rule_api';
|
||||
import { loadActionTypes } from '../../../lib/action_connector_api';
|
||||
import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities';
|
||||
import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
|
||||
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
|
||||
import { RulesDeleteModalConfirmation } from '../../../components/rules_delete_modal_confirmation';
|
||||
import { EmptyPrompt } from '../../../components/prompts/empty_prompt';
|
||||
import { ALERT_STATUS_LICENSE_ERROR } from '../translations';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -92,6 +91,14 @@ import { BulkSnoozeModalWithApi as BulkSnoozeModal } from './bulk_snooze_modal';
|
|||
import { BulkSnoozeScheduleModalWithApi as BulkSnoozeScheduleModal } from './bulk_snooze_schedule_modal';
|
||||
import { useBulkEditSelect } from '../../../hooks/use_bulk_edit_select';
|
||||
import { runRule } from '../../../lib/run_rule';
|
||||
import { bulkDeleteRules } from '../../../lib/rule_api';
|
||||
import {
|
||||
getConfirmDeletionButtonText,
|
||||
getConfirmDeletionModalText,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE,
|
||||
} from '../translations';
|
||||
import { useBulkDeleteResponse } from '../../../hooks/use_bulk_delete_response';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
|
@ -206,6 +213,8 @@ export const RulesList = ({
|
|||
});
|
||||
|
||||
const [rulesToDelete, setRulesToDelete] = useState<string[]>([]);
|
||||
const [rulesToDeleteFilter, setRulesToDeleteFilter] = useState<KueryNode | null | undefined>();
|
||||
const [isDeletingRules, setIsDeletingRules] = useState<boolean>(false);
|
||||
|
||||
// TODO - tech debt: Right now we're using null and undefined to determine if we should
|
||||
// render the bulk edit modal. Refactor this to only keep track of 1 set of rules and types
|
||||
|
@ -256,11 +265,11 @@ export const RulesList = ({
|
|||
);
|
||||
|
||||
const [rulesTypesFilter, hasDefaultRuleTypesFiltersOn] = useMemo(() => {
|
||||
if (isEmpty(typesFilter) && !isEmpty(filteredRuleTypes)) {
|
||||
if (isEmpty(typesFilter)) {
|
||||
return [authorizedRuleTypes.map((art) => art.id), true];
|
||||
}
|
||||
return [typesFilter, false];
|
||||
}, [typesFilter, filteredRuleTypes, authorizedRuleTypes]);
|
||||
}, [typesFilter, authorizedRuleTypes]);
|
||||
|
||||
const { rulesState, setRulesState, loadRules, noData, initialLoad } = useLoadRules({
|
||||
page,
|
||||
|
@ -302,7 +311,7 @@ export const RulesList = ({
|
|||
const isRuleTypeEditableInContext = (ruleTypeId: string) =>
|
||||
ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const refreshRules = useCallback(async () => {
|
||||
if (!ruleTypesState || !hasAnyAuthorizedRuleType) {
|
||||
return;
|
||||
}
|
||||
|
@ -323,8 +332,8 @@ export const RulesList = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData, refresh, percentileOptions]);
|
||||
refreshRules();
|
||||
}, [refreshRules, refresh, percentileOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -621,6 +630,11 @@ export const RulesList = ({
|
|||
setRulesToSnoozeFilter(undefined);
|
||||
};
|
||||
|
||||
const clearRulesToDelete = () => {
|
||||
setRulesToDelete([]);
|
||||
setRulesToDeleteFilter(undefined);
|
||||
};
|
||||
|
||||
const clearRulesToUnsnooze = () => {
|
||||
setRulesToUnsnooze([]);
|
||||
setRulesToUnsnoozeFilter(undefined);
|
||||
|
@ -646,6 +660,7 @@ export const RulesList = ({
|
|||
rulesState.isLoading ||
|
||||
ruleTypesState.isLoading ||
|
||||
isPerformingAction ||
|
||||
isDeletingRules ||
|
||||
isSnoozingRules ||
|
||||
isUnsnoozingRules ||
|
||||
isSchedulingRules ||
|
||||
|
@ -656,6 +671,7 @@ export const RulesList = ({
|
|||
rulesState,
|
||||
ruleTypesState,
|
||||
isPerformingAction,
|
||||
isDeletingRules,
|
||||
isSnoozingRules,
|
||||
isUnsnoozingRules,
|
||||
isSchedulingRules,
|
||||
|
@ -746,7 +762,7 @@ export const RulesList = ({
|
|||
iconType="refresh"
|
||||
onClick={() => {
|
||||
onClearSelection();
|
||||
loadData();
|
||||
refreshRules();
|
||||
}}
|
||||
name="refresh"
|
||||
color="primary"
|
||||
|
@ -824,7 +840,7 @@ export const RulesList = ({
|
|||
/>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
<RulesListAutoRefresh lastUpdate={lastUpdate} onRefresh={loadData} />
|
||||
<RulesListAutoRefresh lastUpdate={lastUpdate} onRefresh={refreshRules} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{rulesStatusesTotal.error > 0 && (
|
||||
|
@ -868,7 +884,7 @@ export const RulesList = ({
|
|||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
onSort={setSort}
|
||||
onPage={setPage}
|
||||
onRuleChanged={() => loadData()}
|
||||
onRuleChanged={() => refreshRules()}
|
||||
onRuleClick={(rule) => {
|
||||
const detailsRoute = ruleDetailsRoute ? ruleDetailsRoute : routeToRuleDetails;
|
||||
history.push(detailsRoute.replace(`:ruleId`, rule.id));
|
||||
|
@ -899,7 +915,7 @@ export const RulesList = ({
|
|||
key={rule.id}
|
||||
item={rule}
|
||||
onLoading={onLoading}
|
||||
onRuleChanged={() => loadData()}
|
||||
onRuleChanged={() => refreshRules()}
|
||||
setRulesToDelete={setRulesToDelete}
|
||||
onEditRule={() => onRuleEdit(rule)}
|
||||
onUpdateAPIKey={setRulesToUpdateAPIKey}
|
||||
|
@ -937,15 +953,17 @@ export const RulesList = ({
|
|||
getFilter={getFilter}
|
||||
onPerformingAction={() => setIsPerformingAction(true)}
|
||||
onActionPerformed={() => {
|
||||
loadData();
|
||||
refreshRules();
|
||||
setIsPerformingAction(false);
|
||||
}}
|
||||
isDeletingRules={isDeletingRules}
|
||||
isSnoozingRules={isSnoozingRules}
|
||||
isUnsnoozingRules={isUnsnoozingRules}
|
||||
isSchedulingRules={isSchedulingRules}
|
||||
isUnschedulingRules={isUnschedulingRules}
|
||||
isUpdatingRuleAPIKeys={isUpdatingRuleAPIKeys}
|
||||
setRulesToDelete={setRulesToDelete}
|
||||
setRulesToDeleteFilter={setRulesToDeleteFilter}
|
||||
setRulesToUpdateAPIKey={setRulesToUpdateAPIKey}
|
||||
setRulesToSnooze={setRulesToSnooze}
|
||||
setRulesToUnsnooze={setRulesToUnsnooze}
|
||||
|
@ -997,34 +1015,54 @@ export const RulesList = ({
|
|||
return table;
|
||||
};
|
||||
|
||||
const [isDeleteModalFlyoutVisible, setIsDeleteModalVisibility] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteModalVisibility(rulesToDelete.length > 0 || Boolean(rulesToDeleteFilter));
|
||||
}, [rulesToDelete, rulesToDeleteFilter]);
|
||||
const { showToast } = useBulkDeleteResponse({ onSearchPopulate });
|
||||
|
||||
const onDeleteCancel = () => {
|
||||
setIsDeleteModalVisibility(false);
|
||||
clearRulesToDelete();
|
||||
};
|
||||
const onDeleteConfirm = useCallback(async () => {
|
||||
setIsDeleteModalVisibility(false);
|
||||
setIsDeletingRules(true);
|
||||
|
||||
const { errors, total } = await bulkDeleteRules({
|
||||
filter: rulesToDeleteFilter,
|
||||
ids: rulesToDelete,
|
||||
http,
|
||||
});
|
||||
|
||||
setIsDeletingRules(false);
|
||||
showToast({ errors, total });
|
||||
await refreshRules();
|
||||
clearRulesToDelete();
|
||||
onClearSelection();
|
||||
}, [http, rulesToDelete, rulesToDeleteFilter, setIsDeletingRules, toasts]);
|
||||
|
||||
const numberRulesToDelete = rulesToDelete.length || numberOfSelectedItems;
|
||||
|
||||
return (
|
||||
<section data-test-subj="rulesList">
|
||||
<DeleteModalConfirmation
|
||||
onDeleted={async () => {
|
||||
setRulesToDelete([]);
|
||||
onClearSelection();
|
||||
await loadData();
|
||||
}}
|
||||
onErrors={async () => {
|
||||
// Refresh the rules from the server, some rules may have beend deleted
|
||||
await loadData();
|
||||
setRulesToDelete([]);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setRulesToDelete([]);
|
||||
}}
|
||||
apiDeleteCall={deleteRules}
|
||||
idsToDelete={rulesToDelete}
|
||||
singleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.singleTitle', {
|
||||
defaultMessage: 'rule',
|
||||
})}
|
||||
multipleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.multipleTitle', {
|
||||
defaultMessage: 'rules',
|
||||
})}
|
||||
setIsLoadingState={(isLoading: boolean) => {
|
||||
setRulesState({ ...rulesState, isLoading });
|
||||
}}
|
||||
/>
|
||||
{isDeleteModalFlyoutVisible && (
|
||||
<RulesDeleteModalConfirmation
|
||||
onConfirm={onDeleteConfirm}
|
||||
onCancel={onDeleteCancel}
|
||||
confirmButtonText={getConfirmDeletionButtonText(
|
||||
numberRulesToDelete,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE
|
||||
)}
|
||||
confirmModalText={getConfirmDeletionModalText(
|
||||
numberRulesToDelete,
|
||||
SINGLE_RULE_TITLE,
|
||||
MULTIPLE_RULE_TITLE
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<BulkSnoozeModal
|
||||
rulesToSnooze={rulesToSnooze}
|
||||
rulesToUnsnooze={rulesToUnsnooze}
|
||||
|
@ -1041,7 +1079,7 @@ export const RulesList = ({
|
|||
clearRulesToSnooze();
|
||||
clearRulesToUnsnooze();
|
||||
onClearSelection();
|
||||
await loadData();
|
||||
await refreshRules();
|
||||
}}
|
||||
onSearchPopulate={onSearchPopulate}
|
||||
/>
|
||||
|
@ -1061,7 +1099,7 @@ export const RulesList = ({
|
|||
clearRulesToSchedule();
|
||||
clearRulesToUnschedule();
|
||||
onClearSelection();
|
||||
await loadData();
|
||||
await refreshRules();
|
||||
}}
|
||||
onSearchPopulate={onSearchPopulate}
|
||||
/>
|
||||
|
@ -1080,7 +1118,7 @@ export const RulesList = ({
|
|||
onUpdated={async () => {
|
||||
clearRulesToUpdateAPIKey();
|
||||
onClearSelection();
|
||||
await loadData();
|
||||
await refreshRules();
|
||||
}}
|
||||
onSearchPopulate={onSearchPopulate}
|
||||
/>
|
||||
|
@ -1095,7 +1133,7 @@ export const RulesList = ({
|
|||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
ruleTypeIndex={ruleTypesState.data}
|
||||
onSave={loadData}
|
||||
onSave={refreshRules}
|
||||
/>
|
||||
)}
|
||||
{editFlyoutVisible && currentRuleToEdit && (
|
||||
|
@ -1109,7 +1147,7 @@ export const RulesList = ({
|
|||
ruleType={
|
||||
ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType<string, string>
|
||||
}
|
||||
onSave={loadData}
|
||||
onSave={refreshRules}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from '@testing-library/react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
|
||||
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
|
||||
import { RulesList } from './rules_list';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
mockedRulesData,
|
||||
ruleTypeFromApi,
|
||||
getDisabledByLicenseRuleTypeFromApi,
|
||||
ruleType,
|
||||
} from './test_helpers';
|
||||
import { IToasts } from '@kbn/core/public';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||
useUiSetting: jest.fn(() => false),
|
||||
useUiSetting$: jest.fn((value: string) => ['0,0']),
|
||||
}));
|
||||
jest.mock('../../../lib/action_connector_api', () => ({
|
||||
loadActionTypes: jest.fn(),
|
||||
loadAllActions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/rule_api', () => ({
|
||||
loadRulesWithKueryFilter: jest.fn(),
|
||||
loadRuleTypes: jest.fn(),
|
||||
loadRuleAggregationsWithKueryFilter: jest.fn(),
|
||||
updateAPIKey: jest.fn(),
|
||||
loadRuleTags: jest.fn(),
|
||||
bulkDeleteRules: jest.fn().mockResolvedValue({ errors: [], total: 10 }),
|
||||
alertingFrameworkHealth: jest.fn(() => ({
|
||||
isSufficientlySecure: true,
|
||||
hasPermanentEncryptionKey: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/rule_api/aggregate_kuery_filter');
|
||||
jest.mock('../../../lib/rule_api/rules_kuery_filter');
|
||||
|
||||
jest.mock('../../../../common/lib/health_api', () => ({
|
||||
triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })),
|
||||
}));
|
||||
jest.mock('../../../../common/lib/config_api', () => ({
|
||||
triggersActionsUiConfig: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({
|
||||
push: jest.fn(),
|
||||
}),
|
||||
useLocation: () => ({
|
||||
pathname: '/triggersActions/rules/',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/capabilities', () => ({
|
||||
hasAllPrivilege: jest.fn(() => true),
|
||||
hasSaveRulesCapability: jest.fn(() => true),
|
||||
hasShowActionsCapability: jest.fn(() => true),
|
||||
hasExecuteActionsCapability: jest.fn(() => true),
|
||||
}));
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const { loadRuleTypes, bulkDeleteRules } = jest.requireMock('../../../lib/rule_api');
|
||||
|
||||
const { loadRulesWithKueryFilter } = jest.requireMock('../../../lib/rule_api/rules_kuery_filter');
|
||||
const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
|
||||
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
||||
ruleTypeRegistry.list.mockReturnValue([ruleType]);
|
||||
actionTypeRegistry.list.mockReturnValue([]);
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
beforeEach(() => {
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock<any, any>).mockImplementation(() => false);
|
||||
});
|
||||
|
||||
// Test are too slow. It's breaking the build. So we skipp it now and waiting for improvment according this ticket:
|
||||
// https://github.com/elastic/kibana/issues/145122
|
||||
describe.skip('Rules list bulk delete', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
async function setup(authorized: boolean = true) {
|
||||
loadRulesWithKueryFilter.mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
total: 6,
|
||||
data: mockedRulesData,
|
||||
});
|
||||
|
||||
loadActionTypes.mockResolvedValue([
|
||||
{
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
},
|
||||
{
|
||||
id: 'test2',
|
||||
name: 'Test2',
|
||||
},
|
||||
]);
|
||||
loadRuleTypes.mockResolvedValue([
|
||||
ruleTypeFromApi,
|
||||
getDisabledByLicenseRuleTypeFromApi(authorized),
|
||||
]);
|
||||
loadAllActions.mockResolvedValue([]);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useKibanaMock().services.actionTypeRegistry = actionTypeRegistry;
|
||||
wrapper = mountWithIntl(<RulesList />);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await setup();
|
||||
useKibanaMock().services.notifications.toasts = {
|
||||
addSuccess: jest.fn(),
|
||||
addError: jest.fn(),
|
||||
addDanger: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
} as unknown as IToasts;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change');
|
||||
wrapper.find('[data-test-subj="selectAllRulesButton"]').at(1).simulate('click');
|
||||
// Unselect something to test filtering
|
||||
wrapper.find('[data-test-subj="checkboxSelectRow-2"]').at(1).simulate('change');
|
||||
wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click');
|
||||
});
|
||||
|
||||
it('can bulk delete', async () => {
|
||||
wrapper.find('button[data-test-subj="bulkDelete"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').exists()).toBeTruthy();
|
||||
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
const filter = bulkDeleteRules.mock.calls[0][0].filter;
|
||||
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkDeleteRules).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ids: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can cancel bulk delete', async () => {
|
||||
wrapper.find('[data-test-subj="bulkDelete"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').exists()).toBeTruthy();
|
||||
wrapper.find('[data-test-subj="confirmModalCancelButton"]').first().simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(bulkDeleteRules).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('Toast', () => {
|
||||
it('should have success toast message', async () => {
|
||||
wrapper.find('button[data-test-subj="bulkDelete"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').exists()).toBeTruthy();
|
||||
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledWith(
|
||||
'Deleted 10 rules'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have warning toast message', async () => {
|
||||
bulkDeleteRules.mockResolvedValue({
|
||||
errors: [
|
||||
{
|
||||
message: 'string',
|
||||
rule: {
|
||||
id: 'string',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 10,
|
||||
});
|
||||
|
||||
wrapper.find('button[data-test-subj="bulkDelete"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').exists()).toBeTruthy();
|
||||
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledTimes(1);
|
||||
expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Deleted 9 rules, 1 rule encountered errors',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should have danger toast message', async () => {
|
||||
bulkDeleteRules.mockResolvedValue({
|
||||
errors: [
|
||||
{
|
||||
message: 'string',
|
||||
rule: {
|
||||
id: 'string',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
});
|
||||
|
||||
wrapper.find('button[data-test-subj="bulkDelete"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').exists()).toBeTruthy();
|
||||
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Failed to delete 1 rule',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -171,7 +171,7 @@ describe('Rules list bulk actions', () => {
|
|||
|
||||
expect(wrapper.find('[data-test-subj="ruleQuickEditButton"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="disableAll"]').first().prop('isDisabled')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="deleteAll"]').first().prop('isDisabled')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="bulkDelete"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('bulk actions', () => {
|
||||
|
@ -198,9 +198,11 @@ describe('Rules list bulk actions', () => {
|
|||
});
|
||||
|
||||
const filter = bulkSnoozeRules.mock.calls[0][0].filter;
|
||||
expect(filter.function).toEqual('not');
|
||||
expect(filter.arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkSnoozeRules).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -229,9 +231,11 @@ describe('Rules list bulk actions', () => {
|
|||
|
||||
const filter = bulkUnsnoozeRules.mock.calls[0][0].filter;
|
||||
|
||||
expect(filter.function).toEqual('not');
|
||||
expect(filter.arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkUnsnoozeRules).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -255,9 +259,11 @@ describe('Rules list bulk actions', () => {
|
|||
|
||||
const filter = bulkSnoozeRules.mock.calls[0][0].filter;
|
||||
|
||||
expect(filter.function).toEqual('not');
|
||||
expect(filter.arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkSnoozeRules).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -288,9 +294,11 @@ describe('Rules list bulk actions', () => {
|
|||
|
||||
const filter = bulkUnsnoozeRules.mock.calls[0][0].filter;
|
||||
|
||||
expect(filter.function).toEqual('not');
|
||||
expect(filter.arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkUnsnoozeRules).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -315,9 +323,11 @@ describe('Rules list bulk actions', () => {
|
|||
|
||||
const filter = bulkUpdateAPIKey.mock.calls[0][0].filter;
|
||||
|
||||
expect(filter.function).toEqual('not');
|
||||
expect(filter.arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
expect(filter.function).toEqual('and');
|
||||
expect(filter.arguments[0].function).toEqual('or');
|
||||
expect(filter.arguments[1].function).toEqual('not');
|
||||
expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id');
|
||||
expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2');
|
||||
|
||||
expect(bulkUpdateAPIKey).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -198,3 +198,108 @@ export const CLEAR_SELECTION = i18n.translate(
|
|||
defaultMessage: 'Clear selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const SINGLE_RULE_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.singleTitle',
|
||||
{
|
||||
defaultMessage: 'rule',
|
||||
}
|
||||
);
|
||||
export const MULTIPLE_RULE_TITLE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.multipleTitle',
|
||||
{
|
||||
defaultMessage: 'rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_BUTTON_TEXT = i18n.translate(
|
||||
'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const getConfirmDeletionModalText = (
|
||||
numIdsToDelete: number,
|
||||
singleTitle: string,
|
||||
multipleTitle: string
|
||||
) =>
|
||||
i18n.translate('xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText', {
|
||||
defaultMessage:
|
||||
"You won't be able to recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.",
|
||||
values: {
|
||||
numIdsToDelete,
|
||||
singleTitle,
|
||||
multipleTitle,
|
||||
},
|
||||
});
|
||||
|
||||
export const getConfirmDeletionButtonText = (
|
||||
numIdsToDelete: number,
|
||||
singleTitle: string,
|
||||
multipleTitle: string
|
||||
) =>
|
||||
i18n.translate('xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel', {
|
||||
defaultMessage:
|
||||
'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ',
|
||||
values: {
|
||||
numIdsToDelete,
|
||||
singleTitle,
|
||||
multipleTitle,
|
||||
},
|
||||
});
|
||||
|
||||
export const getSuccessfulDeletionNotificationText = (
|
||||
numSuccesses: number,
|
||||
singleTitle: string,
|
||||
multipleTitle: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}',
|
||||
values: {
|
||||
numSuccesses,
|
||||
singleTitle,
|
||||
multipleTitle,
|
||||
},
|
||||
}
|
||||
);
|
||||
export const getFailedDeletionNotificationText = (
|
||||
numErrors: number,
|
||||
singleTitle: string,
|
||||
multipleTitle: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}',
|
||||
values: {
|
||||
numErrors,
|
||||
singleTitle,
|
||||
multipleTitle,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const getPartialSuccessDeletionNotificationText = (
|
||||
numberOfSuccess: number,
|
||||
numberOfErrors: number,
|
||||
singleTitle: string,
|
||||
multipleTitle: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.deleteSelectedIdsPartialSuccessNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Deleted {numberOfSuccess, number} {numberOfSuccess, plural, one {{singleTitle}} other {{multipleTitle}}}, {numberOfErrors, number} {numberOfErrors, plural, one {{singleTitle}} other {{multipleTitle}}} encountered errors',
|
||||
values: {
|
||||
numberOfSuccess,
|
||||
numberOfErrors,
|
||||
singleTitle,
|
||||
multipleTitle,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -167,6 +167,11 @@ export enum ActionConnectorMode {
|
|||
ActionForm = 'actionForm',
|
||||
}
|
||||
|
||||
export interface BulkDeleteResponse {
|
||||
errors: BulkOperationError[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ActionParamsProps<TParams> {
|
||||
actionParams: Partial<TParams>;
|
||||
index: number;
|
||||
|
|
|
@ -223,11 +223,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await pageObjects.triggersActionsUI.searchAlerts(secondAlert.name);
|
||||
|
||||
await testSubjects.click('collapsedItemActions');
|
||||
|
||||
await testSubjects.click('deleteRule');
|
||||
await testSubjects.existOrFail('deleteIdsConfirmation');
|
||||
await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton');
|
||||
await testSubjects.missingOrFail('deleteIdsConfirmation');
|
||||
await testSubjects.exists('rulesDeleteIdsConfirmation');
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
|
||||
await retry.try(async () => {
|
||||
const toastTitle = await pageObjects.common.closeToast();
|
||||
|
@ -348,10 +346,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
await testSubjects.click('bulkAction');
|
||||
|
||||
await testSubjects.click('deleteAll');
|
||||
await testSubjects.existOrFail('deleteIdsConfirmation');
|
||||
await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton');
|
||||
await testSubjects.missingOrFail('deleteIdsConfirmation');
|
||||
await testSubjects.click('bulkDelete');
|
||||
await testSubjects.exists('rulesDeleteIdsConfirmation');
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
|
||||
await retry.try(async () => {
|
||||
const toastTitle = await pageObjects.common.closeToast();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue