From 7a5fcc9cb35491c407475c82508cea4138cb9c85 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:19:54 -0800 Subject: [PATCH] [RAM] Add modal when disabling rule to prompt user to untrack alerts (#175363) ## Summary Adds a modal to all of the stack management disable flows to ask the user if they want to untrack orphaned alerts - Rules list -> bulk disable - Rules list -> State dropdown - Rules list -> Action items dropdown - Rule details ### To Test 1. Navigate to rules list or rule details 2. Try to create a rule that generate alerts 3. Disable the rule, use the modal to determine if the alerts should be untracked 4. Check the alerts to see if they were untracked or still tracked (depending on step 3) ![image](/~https://github.com/elastic/kibana/assets/74562234/1d980f51-d243-4742-b856-12a58369c0f9) ### 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> Co-authored-by: Xavier Mouligneau --- .../rule/apis/bulk_disable/schemas/v1.ts | 1 + .../bulk_disable/bulk_disable_rules.test.ts | 129 +++++---- .../bulk_disable/bulk_disable_rules.ts | 16 +- .../methods/bulk_disable/schemas/index.ts | 1 + .../server/routes/disable_rule.test.ts | 1 + .../alerting/server/routes/disable_rule.ts | 12 +- .../bulk_disable/bulk_disable_rules_route.ts | 4 +- .../server/rules_client/methods/disable.ts | 28 +- .../server/rules_client/rules_client.ts | 2 +- .../server/rules_client/tests/disable.test.ts | 53 +++- .../application/lib/rule_api/bulk_disable.ts | 6 +- .../components/rule_quick_edit_buttons.tsx | 262 ++++++++++-------- .../components/untrack_alerts_modal.test.tsx | 53 ++++ .../components/untrack_alerts_modal.tsx | 72 +++++ .../with_bulk_rule_api_operations.test.tsx | 13 +- .../with_bulk_rule_api_operations.tsx | 5 +- .../components/rule_details.test.tsx | 10 +- .../rule_details/components/rule_details.tsx | 42 ++- .../components/rule_status_panel.test.tsx | 107 +++---- .../components/rule_status_panel.tsx | 9 +- .../collapsed_item_actions.test.tsx | 11 +- .../components/collapsed_item_actions.tsx | 90 ++++-- .../components/rule_status_dropdown.tsx | 145 ++++++---- .../rules_list/components/rules_list.tsx | 10 +- .../rules_list_bulk_disable.test.tsx | 50 +++- .../components/rules_list_table.tsx | 13 +- .../triggers_actions_ui/public/types.ts | 8 + .../common/lib/alert_utils.ts | 7 +- .../tests/alerting/group1/disable.ts | 63 ++++- .../tests/alerting/group4/bulk_disable.ts | 130 +++++++++ .../tests/alerting/group4/index.ts | 1 + .../apps/triggers_actions_ui/details.ts | 3 + .../rules_list/rules_list.ts | 121 +++++++- .../apps/observability/pages/rules_page.ts | 4 + .../observability/rules/rules_list.ts | 8 + 35 files changed, 1100 insertions(+), 390 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts index 3e74ed88ff0f8..41a727b8b19b6 100644 --- a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts @@ -10,4 +10,5 @@ import { schema } from '@kbn/config-schema'; export const bulkDisableRulesRequestBodySchema = schema.object({ filter: schema.maybe(schema.string()), ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), + untrack: schema.maybe(schema.boolean({ defaultValue: false })), }); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts index 81a8466e23a22..3f43d6077eb35 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts @@ -41,14 +41,6 @@ import { import { migrateLegacyActions } from '../../../../rules_client/lib'; import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; -jest.mock('../../../../task_runner/alert_task_instance', () => ({ - taskInstanceToAlertTaskInstance: jest.fn(), -})); - -const { taskInstanceToAlertTaskInstance } = jest.requireMock( - '../../../../task_runner/alert_task_instance' -); - jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => { return { migrateLegacyActions: jest.fn().mockResolvedValue({ @@ -63,6 +55,12 @@ jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invali bulkMarkApiKeysForInvalidation: jest.fn(), })); +jest.mock('../../../../rules_client/lib/untrack_rule_alerts', () => ({ + untrackRuleAlerts: jest.fn(), +})); + +const { untrackRuleAlerts } = jest.requireMock('../../../../rules_client/lib/untrack_rule_alerts'); + const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); @@ -192,6 +190,76 @@ describe('bulkDisableRules', () => { }); }); + test('should call untrack alert if untrack is true', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], + }); + + const result = await rulesClient.bulkDisableRules({ filter: 'fake_filter', untrack: true }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + id: 'id1', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + expect.objectContaining({ + id: 'id2', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + ]), + { overwrite: true } + ); + + expect(result).toStrictEqual({ + errors: [], + rules: [returnedRuleForBulkDisable1, returnedRuleForBulkDisable2], + total: 2, + }); + + expect(untrackRuleAlerts).toHaveBeenCalled(); + }); + + test('should not call untrack alert if untrack is false', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], + }); + + const result = await rulesClient.bulkDisableRules({ filter: 'fake_filter', untrack: true }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + id: 'id1', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + expect.objectContaining({ + id: 'id2', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + ]), + { overwrite: true } + ); + + expect(result).toStrictEqual({ + errors: [], + rules: [returnedRuleForBulkDisable1, returnedRuleForBulkDisable2], + total: 2, + }); + + expect(untrackRuleAlerts).toHaveBeenCalled(); + }); + test('should try to disable rules, one successful and one with 500 error', async () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [disabledRuleForBulkDisable1, savedObjectWith500Error], @@ -585,51 +653,6 @@ describe('bulkDisableRules', () => { }); }); - describe('recoverRuleAlerts', () => { - beforeEach(() => { - taskInstanceToAlertTaskInstance.mockImplementation(() => ({ - state: { - alertInstances: { - '1': { - meta: { - lastScheduledActions: { - group: 'default', - date: new Date().toISOString(), - }, - }, - state: { bar: false }, - }, - }, - }, - })); - }); - test('should call logEvent', async () => { - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], - }); - - await rulesClient.bulkDisableRules({ filter: 'fake_filter' }); - - expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); - }); - - test('should call logger.warn', async () => { - eventLogger.logEvent.mockImplementation(() => { - throw new Error('UPS'); - }); - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], - }); - - await rulesClient.bulkDisableRules({ filter: 'fake_filter' }); - - expect(logger.warn).toHaveBeenCalledTimes(2); - expect(logger.warn).toHaveBeenLastCalledWith( - "rulesClient.disable('id2') - Could not write untrack events - UPS" - ); - }); - }); - describe('legacy actions migration for SIEM', () => { test('should call migrateLegacyActions', async () => { encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts index 0ac84ce2ef6d7..84229c4dc665e 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts @@ -50,7 +50,7 @@ export const bulkDisableRules = async ( throw Boom.badRequest(`Error validating bulk disable data - ${error.message}`); } - const { ids, filter } = options; + const { ids, filter, untrack = false } = options; const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); const authorizationFilter = await getAuthorizationFilter(context, { action: 'DISABLE' }); @@ -72,7 +72,7 @@ export const bulkDisableRules = async ( action: 'DISABLE', logger: context.logger, bulkOperation: (filterKueryNode: KueryNode | null) => - bulkDisableRulesWithOCC(context, { filter: filterKueryNode }), + bulkDisableRulesWithOCC(context, { filter: filterKueryNode, untrack }), filter: kueryNodeFilterWithAuth, }) ); @@ -120,7 +120,13 @@ export const bulkDisableRules = async ( const bulkDisableRulesWithOCC = async ( context: RulesClientContext, - { filter }: { filter: KueryNode | null } + { + filter, + untrack = false, + }: { + filter: KueryNode | null; + untrack: boolean; + } ) => { const additionalFilter = nodeBuilder.is('alert.attributes.enabled', 'true'); @@ -151,7 +157,9 @@ const bulkDisableRulesWithOCC = async ( for await (const response of rulesFinder.find()) { await pMap(response.saved_objects, async (rule) => { try { - await untrackRuleAlerts(context, rule.id, rule.attributes); + if (untrack) { + await untrackRuleAlerts(context, rule.id, rule.attributes); + } if (rule.attributes.name) { ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts index 3e74ed88ff0f8..41a727b8b19b6 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts @@ -10,4 +10,5 @@ import { schema } from '@kbn/config-schema'; export const bulkDisableRulesRequestBodySchema = schema.object({ filter: schema.maybe(schema.string()), ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), + untrack: schema.maybe(schema.boolean({ defaultValue: false })), }); diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.test.ts b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts index 6f4806b8e17f5..6186fbd8a5db7 100644 --- a/x-pack/plugins/alerting/server/routes/disable_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts @@ -52,6 +52,7 @@ describe('disableRuleRoute', () => { Array [ Object { "id": "1", + "untrack": false, }, ] `); diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.ts b/x-pack/plugins/alerting/server/routes/disable_rule.ts index 99795e7ee2bf2..726b080bedbf1 100644 --- a/x-pack/plugins/alerting/server/routes/disable_rule.ts +++ b/x-pack/plugins/alerting/server/routes/disable_rule.ts @@ -15,6 +15,14 @@ const paramSchema = schema.object({ id: schema.string(), }); +const bodySchema = schema.nullable( + schema.maybe( + schema.object({ + untrack: schema.maybe(schema.boolean({ defaultValue: false })), + }) + ) +); + export const disableRuleRoute = ( router: IRouter, licenseState: ILicenseState @@ -24,14 +32,16 @@ export const disableRuleRoute = ( path: `${BASE_ALERTING_API_PATH}/rule/{id}/_disable`, validate: { params: paramSchema, + body: bodySchema, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); const { id } = req.params; + const { untrack = false } = req.body || {}; try { - await rulesClient.disable({ id }); + await rulesClient.disable({ id, untrack }); return res.noContent(); } catch (e) { if (e instanceof RuleTypeDisabledError) { diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts index 03afb4a95d25a..39b81ee74fe91 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts @@ -39,10 +39,10 @@ export const bulkDisableRulesRoute = ({ const rulesClient = (await context.alerting).getRulesClient(); const body: BulkDisableRulesRequestBodyV1 = req.body; - const { filter, ids } = body; + const { filter, ids, untrack } = body; try { - const bulkDisableResults = await rulesClient.bulkDisableRules({ filter, ids }); + const bulkDisableResults = await rulesClient.bulkDisableRules({ filter, ids, untrack }); const resultBody: BulkDisableRulesResponseV1 = { body: { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts index 38b0dcc7e17d6..88396559031c8 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts @@ -15,15 +15,33 @@ import { untrackRuleAlerts, updateMeta, migrateLegacyActions } from '../lib'; import { RuleAttributes } from '../../data/rule/types'; import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -export async function disable(context: RulesClientContext, { id }: { id: string }): Promise { +export async function disable( + context: RulesClientContext, + { + id, + untrack = false, + }: { + id: string; + untrack?: boolean; + } +): Promise { return await retryIfConflicts( context.logger, `rulesClient.disable('${id}')`, - async () => await disableWithOCC(context, { id }) + async () => await disableWithOCC(context, { id, untrack }) ); } -async function disableWithOCC(context: RulesClientContext, { id }: { id: string }) { +async function disableWithOCC( + context: RulesClientContext, + { + id, + untrack = false, + }: { + id: string; + untrack?: boolean; + } +) { let attributes: RawRule; let version: string | undefined; let references: SavedObjectReference[]; @@ -70,7 +88,9 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string throw error; } - await untrackRuleAlerts(context, id, attributes as RuleAttributes); + if (untrack) { + await untrackRuleAlerts(context, id, attributes as RuleAttributes); + } context.auditLogger?.log( ruleAuditEvent({ diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 0a2da42d7e424..39b4353525f7e 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -160,7 +160,7 @@ export class RulesClient { public updateApiKey = (options: { id: string }) => updateApiKey(this.context, options); public enable = (options: { id: string }) => enable(this.context, options); - public disable = (options: { id: string }) => disable(this.context, options); + public disable = (options: { id: string; untrack?: boolean }) => disable(this.context, options); public snooze = (options: SnoozeRuleOptions) => snoozeRule(this.context, options); public unsnooze = (options: UnsnoozeParams) => unsnoozeRule(this.context, options); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index 4f539f5a9f7b1..9e7073da8a18d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -299,7 +299,7 @@ describe('disable()', () => { }, ownerId: null, }); - await rulesClient.disable({ id: '1' }); + await rulesClient.disable({ id: '1', untrack: true }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -388,7 +388,7 @@ describe('disable()', () => { test('disables the rule even if unable to retrieve task manager doc to generate untrack event log events', async () => { taskManager.get.mockRejectedValueOnce(new Error('Fail')); - await rulesClient.disable({ id: '1' }); + await rulesClient.disable({ id: '1', untrack: true }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -440,6 +440,55 @@ describe('disable()', () => { ); }); + test('should not untrack rule alert if untrack is false', async () => { + await rulesClient.disable({ id: '1', untrack: false }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( + RULE_SAVED_OBJECT_TYPE, + '1', + { + namespace: 'default', + } + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + RULE_SAVED_OBJECT_TYPE, + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + revision: 0, + scheduledTaskId: '1', + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + nextRun: null, + }, + { + version: '123', + } + ); + expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.get).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalled(); + }); + test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await rulesClient.disable({ id: '1' }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts index 4e322d494d7df..437d2ae016293 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts @@ -5,16 +5,18 @@ * 2.0. */ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { BulkOperationResponse, BulkOperationAttributes } from '../../../types'; +import { BulkOperationResponse, BulkDisableParams } from '../../../types'; export const bulkDisableRules = async ({ filter, ids, http, -}: BulkOperationAttributes): Promise => { + untrack, +}: BulkDisableParams): Promise => { try { const body = JSON.stringify({ ids: ids?.length ? ids : undefined, + untrack, ...(filter ? { filter: JSON.stringify(filter) } : {}), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx index fa7fe6bae44d5..9327733f3b5ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { KueryNode } from '@kbn/es-query'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -18,6 +18,7 @@ import { } from './with_bulk_rule_api_operations'; import './rule_quick_edit_buttons.scss'; import { useKibana } from '../../../../common/lib/kibana'; +import { UntrackAlertsModal } from './untrack_alerts_modal'; export type ComponentOpts = { selectedItems: RuleTableItem[]; @@ -29,7 +30,7 @@ export type ComponentOpts = { isEnablingRules?: boolean; isDisablingRules?: boolean; isBulkEditing?: boolean; - onDisable: () => Promise; + onDisable: (untrack: boolean) => Promise; onEnable: () => Promise; updateRulesToBulkEdit: (props: UpdateRulesToBulkEditProps) => void; } & BulkOperationsComponentOpts; @@ -52,6 +53,8 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ notifications: { toasts }, } = useKibana().services; + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); + const isPerformingAction = isEnablingRules || isDisablingRules || isBulkEditing; const hasDisabledByLicenseRuleTypes = useMemo(() => { @@ -229,125 +232,146 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ } } + const onDisableClick = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onModalConfirm = useCallback( + (untrack: boolean) => { + onModalClose(); + onDisable(untrack); + }, + [onModalClose, onDisable] + ); + return ( - - {!isAllSelected && ( - <> - - - - - - - - - - - - - - - - - - - - - + <> + + {!isAllSelected && ( + <> + + + + + + + + + + + + + + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + {isUntrackAlertsModalOpen && ( + )} - - - - - - - - - - - - - - - - - - - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx new file mode 100644 index 0000000000000..78d06175c580c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import { UntrackAlertsModal } from './untrack_alerts_modal'; + +const onConfirmMock = jest.fn(); + +const onCancelMock = jest.fn(); + +describe('Untrack alerts modal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly', () => { + render(); + + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); + }); + + it('should track alerts', () => { + render(); + + fireEvent.click(screen.getByTestId('untrackAlertsModalSwitch')); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(onConfirmMock).toHaveBeenCalledWith(true); + }); + + it('should untrack alerts', () => { + render(); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(onConfirmMock).toHaveBeenCalledWith(false); + }); + + it('should close if cancel is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('confirmModalCancelButton')); + + expect(onCancelMock).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx new file mode 100644 index 0000000000000..f3ad676e5fc21 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx @@ -0,0 +1,72 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; + +const UNTRACK_ORPHANED_ALERTS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.title', + { + defaultMessage: 'Disable rule', + } +); + +const UNTRACK_ORPHANED_ALERTS_CONFIRM_BUTTON_TEXT = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.confirmButtonText', + { + defaultMessage: 'Disable rule', + } +); + +const UNTRACK_ORPHANED_ALERTS_CANCEL_BUTTON_TEXT = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.cancelButtonText', + { + defaultMessage: 'cancel', + } +); + +const UNTRACK_ORPHANED_ALERTS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.toggleLabel', + { + defaultMessage: + 'When disabling, all alerts related to this rule will be updated to "Untracked"', + } +); + +export interface UntrackAlertsModalProps { + onCancel: () => void; + onConfirm: (untrack: boolean) => void; +} + +export const UntrackAlertsModal = (props: UntrackAlertsModalProps) => { + const { onCancel, onConfirm } = props; + + const [isUntrack, setIsUntrack] = useState(false); + + const onChange = useCallback((e: EuiSwitchEvent) => { + setIsUntrack(e.target.checked); + }, []); + + return ( + onConfirm(isUntrack)} + confirmButtonText={UNTRACK_ORPHANED_ALERTS_CONFIRM_BUTTON_TEXT} + cancelButtonText={UNTRACK_ORPHANED_ALERTS_CANCEL_BUTTON_TEXT} + > + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx index 4fba7e7027d8c..5a283ff60ec72 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx @@ -139,7 +139,11 @@ describe('with_bulk_rule_api_operations', () => { it('disableRule calls the disableRule api', () => { const { http } = useKibanaMock().services; const ComponentToExtend = ({ bulkDisableRules, rule }: ComponentOpts & { rule: Rule }) => { - return ; + return ( + + ); }; const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); @@ -148,7 +152,7 @@ describe('with_bulk_rule_api_operations', () => { component.find('button').simulate('click'); expect(bulkDisableRules).toHaveBeenCalledTimes(1); - expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id], http }); + expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id], http, untrack: true }); }); // bulk rules @@ -212,7 +216,9 @@ describe('with_bulk_rule_api_operations', () => { const { http } = useKibanaMock().services; const ComponentToExtend = ({ bulkDisableRules, rules }: ComponentOpts & { rules: Rule[] }) => { return ( - ); @@ -227,6 +233,7 @@ describe('with_bulk_rule_api_operations', () => { expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http, + untrack: true, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx index ecb431ad3ba93..81a4f27ef5e1c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx @@ -23,6 +23,7 @@ import { BulkEditResponse, BulkOperationResponse, BulkOperationAttributesWithoutHttp, + BulkDisableParamsWithoutHttp, } from '../../../../types'; import type { LoadExecutionLogAggregationsProps, @@ -92,7 +93,7 @@ export interface ComponentOpts { cloneRule: (ruleId: string) => Promise; bulkDeleteRules: (props: BulkOperationAttributesWithoutHttp) => Promise; bulkEnableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; - bulkDisableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; + bulkDisableRules: (props: BulkDisableParamsWithoutHttp) => Promise; } export type PropsWithOptionalApiHandlers = Omit & Partial; @@ -199,7 +200,7 @@ export function withBulkRuleOperations( bulkEnableRules={async (bulkEnableProps: BulkOperationAttributesWithoutHttp) => { return await bulkEnableRules({ http, ...bulkEnableProps }); }} - bulkDisableRules={async (bulkDisableProps: BulkOperationAttributesWithoutHttp) => { + bulkDisableRules={async (bulkDisableProps: BulkDisableParamsWithoutHttp) => { return await bulkDisableRules({ http, ...bulkDisableProps }); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index 3bd45741714a0..7d292c5dee3cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -772,8 +772,16 @@ describe('rule_details', () => { disableButton.simulate('click'); + const modal = wrapper.find('[data-test-subj="untrackAlertsModal"]'); + expect(modal.exists()).toBeTruthy(); + + modal.find('[data-test-subj="confirmModalConfirmButton"]').last().simulate('click'); + expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledTimes(1); - expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id] }); + expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledWith({ + ids: [rule.id], + untrack: false, + }); }); it('should enable the rule when clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index 6bae4f720c032..a7f46a8e554c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -70,6 +70,7 @@ import { } from '../../rules_list/translations'; import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; import { RefreshToken } from './types'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type RuleDetailsProps = { rule: Rule; @@ -115,6 +116,7 @@ export const RuleDetails: React.FunctionComponent = ({ const [rulesToDelete, setRulesToDelete] = useState([]); const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState(false); @@ -288,11 +290,39 @@ export const RuleDetails: React.FunctionComponent = ({ setRulesToDelete([]); goToRulesList(); }; + const onDeleteCancel = () => { setIsDeleteModalVisibility(false); setRulesToDelete([]); }; + const onDisableModalOpen = () => { + setIsUntrackAlertsModalOpen(true); + }; + + const onDisableModalClose = () => { + setIsUntrackAlertsModalOpen(false); + }; + + const onEnable = async () => { + await bulkEnableRules({ ids: [rule.id] }); + requestRefresh(); + }; + + const onDisable = async (untrack: boolean) => { + onDisableModalClose(); + await bulkDisableRules({ ids: [rule.id], untrack }); + requestRefresh(); + }; + + const onEnableDisable = (enable: boolean) => { + if (enable) { + onEnable(); + } else { + onDisableModalOpen(); + } + }; + return ( <> {isDeleteModalFlyoutVisible && ( @@ -311,6 +341,9 @@ export const RuleDetails: React.FunctionComponent = ({ )} /> )} + {isUntrackAlertsModalOpen && ( + + )} { setRulesToUpdateAPIKey([]); @@ -400,14 +433,7 @@ export const RuleDetails: React.FunctionComponent = ({ onApiKeyUpdate={(ruleId) => { setRulesToUpdateAPIKey([ruleId]); }} - onEnableDisable={async (enable) => { - if (enable) { - await bulkEnableRules({ ids: [rule.id] }); - } else { - await bulkDisableRules({ ids: [rule.id] }); - } - requestRefresh(); - }} + onEnableDisable={onEnableDisable} onRunRule={onRunRule} />, editButton, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx index 69dab6587d6f0..ebe28a2636378 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx @@ -6,6 +6,14 @@ */ import React from 'react'; +import { + render, + screen, + waitFor, + waitForElementToBeRemoved, + fireEvent, +} from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { act } from 'react-dom/test-utils'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; @@ -103,35 +111,33 @@ describe('rule status panel', () => { it('should disable the rule when picking disable in the dropdown', async () => { const rule = mockRule({ enabled: true }); const bulkDisableRules = jest.fn(); - const wrapper = mountWithIntl( - + render( + + + ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + if (screen.queryByTestId('centerJustifiedSpinner')) { + await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner')); + } - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('button.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - await nextTick(); - }); + fireEvent.click(screen.getByTestId('ruleStatusDropdownBadge')); + + fireEvent.click(screen.getByTestId('statusDropdownDisabledItem')); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); - expect(bulkDisableRules).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('progressbar')).toBeInTheDocument(); + + await waitFor(() => expect(bulkDisableRules).toHaveBeenCalledTimes(1)); }); it('if rule is already disabled should do nothing when picking disable in the dropdown', async () => { @@ -233,55 +239,4 @@ describe('rule status panel', () => { expect(bulkEnableRules).toHaveBeenCalledTimes(0); }); - - it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => { - const rule = mockRule({ - enabled: true, - }); - - const bulkDisableRules = jest.fn(async () => { - await new Promise((resolve) => setTimeout(resolve, 6000)); - }) as any; - - const wrapper = mountWithIntl( - - ); - - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('button.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - await act(async () => { - expect(bulkDisableRules).toHaveBeenCalled(); - expect( - wrapper.find('[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner') - .length - ).toBeGreaterThan(0); - }); - }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx index a7b87cc722530..2419993f12670 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx @@ -102,6 +102,13 @@ export const RuleStatusPanel: React.FC = ({ requestRefresh(); }, [requestRefresh, loadEventLogs]); + const onDisableRule = useCallback( + (untrack: boolean) => { + return bulkDisableRules({ ids: [rule.id], untrack }); + }, + [bulkDisableRules, rule.id] + ); + useEffect(() => { if (isInitialized.current) { loadEventLogs(); @@ -126,7 +133,7 @@ export const RuleStatusPanel: React.FC = ({ bulkDisableRules({ ids: [rule.id] })} + disableRule={onDisableRule} enableRule={() => bulkEnableRules({ ids: [rule.id] })} snoozeRule={async () => {}} unsnoozeRule={async () => {}} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index 8d5b7952b5f00..9bcab7c092421 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -224,11 +224,20 @@ describe('CollapsedItemActions', () => { wrapper.update(); }); wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + + const modal = wrapper.find('[data-test-subj="untrackAlertsModal"]'); + expect(modal.exists()).toBeTruthy(); + + modal.find('[data-test-subj="confirmModalConfirmButton"]').last().simulate('click'); + await act(async () => { await tick(10); wrapper.update(); }); - expect(bulkDisableRules).toHaveBeenCalled(); + expect(bulkDisableRules).toHaveBeenCalledWith({ + ids: ['1'], + untrack: false, + }); }); test('handles case when rule is unmuted and disabled and enable is clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 7df59161dffeb..5a6217f31f83f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -34,6 +34,7 @@ import { SNOOZE_SUCCESS_MESSAGE, UNSNOOZE_SUCCESS_MESSAGE, } from './notify_badge'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type ComponentOpts = { item: RuleTableItem; @@ -70,6 +71,8 @@ export const CollapsedItemActions: React.FunctionComponent = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isDisabled, setIsDisabled] = useState(!item.enabled); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); + useEffect(() => { setIsDisabled(!item.enabled); }, [item.enabled]); @@ -179,6 +182,42 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ]; }, [isDisabled, item, snoozedButtonText]); + const onDisableModalOpen = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onDisableModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onEnable = useCallback(async () => { + asyncScheduler.schedule(async () => { + await bulkEnableRules({ ids: [item.id] }); + onRuleChanged(); + }, 10); + setIsDisabled(false); + setIsPopoverOpen(false); + }, [bulkEnableRules, onRuleChanged, item.id]); + + const onDisable = useCallback( + async (untrack: boolean) => { + onDisableModalClose(); + await bulkDisableRules({ ids: [item.id], untrack }); + onRuleChanged(); + setIsDisabled(true); + setIsPopoverOpen(false); + }, + [onDisableModalClose, bulkDisableRules, onRuleChanged, item.id] + ); + + const onDisableClick = useCallback(() => { + if (isDisabled) { + onEnable(); + } else { + onDisableModalOpen(); + } + }, [isDisabled, onEnable, onDisableModalOpen]); + const panels = [ { id: 0, @@ -191,19 +230,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ { disabled: !item.isEditable || !item.enabledInLicense, 'data-test-subj': 'disableButton', - onClick: async () => { - const enabled = !isDisabled; - asyncScheduler.schedule(async () => { - if (enabled) { - await bulkDisableRules({ ids: [item.id] }); - } else { - await bulkEnableRules({ ids: [item.id] }); - } - onRuleChanged(); - }, 10); - setIsDisabled(!isDisabled); - setIsPopoverOpen(!isPopoverOpen); - }, + onClick: onDisableClick, name: isDisabled ? i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle', @@ -310,22 +337,27 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ]; return ( - setIsPopoverOpen(false)} - ownFocus - panelPaddingSize="none" - data-test-subj="collapsedItemActions" - > - - + <> + setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="none" + data-test-subj="collapsedItemActions" + > + + + {isUntrackAlertsModalOpen && ( + + )} + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 145fda4e4addd..1470fe2606107 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -27,6 +27,7 @@ import { SnoozePanel } from './rule_snooze'; import { isRuleSnoozed } from '../../../lib'; import { Rule, SnoozeSchedule, BulkOperationResponse } from '../../../../types'; import { ToastWithCircuitBreakerContent } from '../../../components/toast_with_circuit_breaker_content'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; const SNOOZE_END_TIME_FORMAT = 'LL @ LT'; @@ -40,7 +41,7 @@ export interface ComponentOpts { rule: DropdownRuleRecord; onRuleChanged: () => void; enableRule: () => Promise; - disableRule: () => Promise; + disableRule: (untrack: boolean) => Promise; snoozeRule: (snoozeSchedule: SnoozeSchedule) => Promise; unsnoozeRule: (scheduleIds?: string[]) => Promise; isEditable: boolean; @@ -74,6 +75,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ }, [rule, hideSnoozeOption]); const [isUpdating, setIsUpdating] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); const onClickBadge = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), [setIsPopoverOpen]); const onClosePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); @@ -97,25 +99,59 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ throw new Error(); }, [enableRule, toasts]); - const onChangeEnabledStatus = useCallback( - async (enable: boolean) => { - if (rule.enabled === enable) { - return; - } + const onEnable = useCallback(async () => { + setIsUpdating(true); + try { + await enableRuleInternal(); + setIsEnabled(true); + onRuleChanged(); + } finally { + setIsUpdating(false); + } + }, [onRuleChanged, enableRuleInternal]); + + const onDisable = useCallback( + async (untrack: boolean) => { setIsUpdating(true); try { - if (enable) { - await enableRuleInternal(); - } else { - await disableRule(); - } - setIsEnabled(!isEnabled); + await disableRule(untrack); + setIsEnabled(false); onRuleChanged(); } finally { setIsUpdating(false); } }, - [rule.enabled, isEnabled, onRuleChanged, enableRuleInternal, disableRule] + [onRuleChanged, disableRule] + ); + + const onDisableModalOpen = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onDisableModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onModalConfirm = useCallback( + (untrack: boolean) => { + onDisableModalClose(); + onDisable(untrack); + }, + [onDisableModalClose, onDisable] + ); + + const onChangeEnabledStatus = useCallback( + async (enable: boolean) => { + if (rule.enabled === enable) { + return; + } + if (enable) { + await onEnable(); + } else { + onDisableModalOpen(); + } + }, + [rule.enabled, onEnable, onDisableModalOpen] ); const onSnoozeRule = useCallback( @@ -168,6 +204,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ const editableBadge = ( = ({ ); return ( - - - {isEditable ? ( - - - - ) : ( - nonEditableBadge - )} - - - {remainingSnoozeTime} - - + <> + + + {isEditable ? ( + + + + ) : ( + nonEditableBadge + )} + + + {remainingSnoozeTime} + + + {isUntrackAlertsModalOpen && ( + + )} + ); }; @@ -260,6 +302,7 @@ const RuleStatusMenu: React.FunctionComponent = ({ } onClosePopover(); }, [onChangeEnabledStatus, onClosePopover, unsnoozeRule, isSnoozed]); + const disableRule = useCallback(() => { onChangeEnabledStatus(false); onClosePopover(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 67d475ae2689e..9b66a65949674 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -555,8 +555,8 @@ export const RulesList = ({ }; const onDisableRule = useCallback( - (rule: RuleTableItem) => { - return bulkDisableRules({ http, ids: [rule.id] }); + (rule: RuleTableItem, untrack: boolean) => { + return bulkDisableRules({ http, ids: [rule.id], untrack }); }, [bulkDisableRules] ); @@ -701,12 +701,12 @@ export const RulesList = ({ onClearSelection(); }; - const onDisable = async () => { + const onDisable = async (untrack: boolean) => { setIsDisablingRules(true); const { errors, total } = isAllSelected - ? await bulkDisableRules({ http, filter: getFilter() }) - : await bulkDisableRules({ http, ids: selectedIds }); + ? await bulkDisableRules({ http, filter: getFilter(), untrack }) + : await bulkDisableRules({ http, ids: selectedIds, untrack }); setIsDisablingRules(false); showToast({ action: 'DISABLE', errors, total }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx index 4110ea34d3c95..40aad0464e4f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx @@ -7,10 +7,10 @@ import * as React from 'react'; import { IToasts } from '@kbn/core/public'; import { - act, render, screen, cleanup, + waitFor, waitForElementToBeRemoved, fireEvent, } from '@testing-library/react'; @@ -180,10 +180,16 @@ describe('Rules list Bulk Disable', () => { }); it('can bulk disable', async () => { - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + const filter = bulkDisableRules.mock.calls[0][0].filter; expect(filter.function).toEqual('and'); @@ -192,23 +198,25 @@ describe('Rules list Bulk Disable', () => { 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(bulkDisableRules).toHaveBeenCalledWith( - expect.not.objectContaining({ - ids: [], - }) - ); + expect(bulkDisableRules).toHaveBeenCalled(); + expect(screen.getByTestId('checkboxSelectRow-1').closest('tr')).not.toHaveClass( 'euiTableRow-isSelected' ); - expect(screen.queryByTestId('bulkDisable')).not.toBeInTheDocument(); }); describe('Toast', () => { it('should have success toast message', async () => { - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledWith( 'Disabled 10 rules' @@ -229,10 +237,16 @@ describe('Rules list Bulk Disable', () => { total: 10, }); - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledWith( expect.objectContaining({ @@ -255,10 +269,16 @@ describe('Rules list Bulk Disable', () => { total: 1, }); - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx index 9933a52e3ac1f..0f1f42b475a35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx @@ -127,7 +127,7 @@ export interface RulesListTableProps { onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void; onRuleChanged: () => Promise; onEnableRule: (rule: RuleTableItem) => Promise; - onDisableRule: (rule: RuleTableItem) => Promise; + onDisableRule: (rule: RuleTableItem, untrack: boolean) => Promise; onSnoozeRule: (rule: RuleTableItem, snoozeSchedule: SnoozeSchedule) => Promise; onUnsnoozeRule: (rule: RuleTableItem, scheduleIds?: string[]) => Promise; onSelectAll: () => void; @@ -281,12 +281,19 @@ export const RulesListTable = (props: RulesListTableProps) => { [ruleTypeRegistry] ); + const onDisableRuleInternal = useCallback( + (rule: RuleTableItem) => (untrack: boolean) => { + return onDisableRule(rule, untrack); + }, + [onDisableRule] + ); + const renderRuleStatusDropdown = useCallback( (rule: RuleTableItem) => { return ( await onDisableRule(rule)} + disableRule={onDisableRuleInternal(rule)} enableRule={async () => await onEnableRule(rule)} snoozeRule={async () => {}} unsnoozeRule={async () => {}} @@ -296,7 +303,7 @@ export const RulesListTable = (props: RulesListTableProps) => { /> ); }, - [isRuleTypeEditableInContext, onDisableRule, onEnableRule, onRuleChanged] + [isRuleTypeEditableInContext, onDisableRuleInternal, onEnableRule, onRuleChanged] ); const selectionColumn = useMemo(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 36cc294bbda5f..ba5afb74ecfd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -221,6 +221,14 @@ export type BulkOperationAttributes = BulkOperationAttributesWithoutHttp & { http: HttpSetup; }; +export type BulkDisableParamsWithoutHttp = BulkOperationAttributesWithoutHttp & { + untrack: boolean; +}; + +export type BulkDisableParams = BulkDisableParamsWithoutHttp & { + http: HttpSetup; +}; + export interface ActionParamsProps { actionParams: Partial; index: number; diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 3307211694cc0..6cf75ee6ad635 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -101,10 +101,13 @@ export class AlertUtils { return request; } - public getDisableRequest(alertId: string) { + public getDisableRequest(alertId: string, untrack?: boolean) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/_disable`) - .set('kbn-xsrf', 'foo'); + .set('kbn-xsrf', 'foo') + .send({ + untrack: untrack === undefined ? true : untrack, + }); if (this.user) { return request.auth(this.user.username, this.user.password); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts index 0dccb0ea5a545..32d8a4dc3c650 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; import { Spaces } from '../../../scenarios'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -20,6 +22,8 @@ import { } from '../../../../common/lib'; import { validateEvent } from './event_log'; +const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; + // eslint-disable-next-line import/no-default-export export default function createDisableRuleTests({ getService }: FtrProviderContext) { const es = getService('es'); @@ -31,7 +35,16 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex const objectRemover = new ObjectRemover(supertestWithoutAuth); const ruleUtils = new RuleUtils({ space: Spaces.space1, supertestWithoutAuth }); - after(() => objectRemover.removeAll()); + afterEach(async () => { + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { + match_all: {}, + }, + conflicts: 'proceed', + }); + await objectRemover.removeAll(); + }); async function getScheduledTask(id: string): Promise { const scheduledTask = await es.get({ @@ -172,6 +185,54 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex }); }); + it('should not untrack alerts if untrack is false', async () => { + const { body: createdRule } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data', + schedule: { interval: '24h' }, + throttle: undefined, + notify_when: undefined, + params: { + index: ES_TEST_INDEX_NAME, + reference: 'test', + }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + await retry.try(async () => { + const { + hits: { hits: activeAlerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + + expect(activeAlerts.length).eql(2); + activeAlerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await ruleUtils.getDisableRequest(createdRule.id, false); + + const { + hits: { hits: untrackedAlerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + expect(untrackedAlerts.length).eql(2); + untrackedAlerts.forEach((untrackedAlert: any) => { + expect(untrackedAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + it('should disable rule even if associated task manager document is missing', async () => { const { body: createdRule } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts new file mode 100644 index 0000000000000..e5692e73a15ab --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts @@ -0,0 +1,130 @@ +/* + * 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 expect from '@kbn/expect'; +import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; + +const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; + +// eslint-disable-next-line import/no-default-export +export default function createDisableRuleTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const retry = getService('retry'); + const supertest = getService('supertest'); + + describe('bulkDisable', () => { + const objectRemover = new ObjectRemover(supertest); + + const createRule = async () => { + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data', + schedule: { interval: '24h' }, + throttle: undefined, + notify_when: undefined, + params: { + index: ES_TEST_INDEX_NAME, + reference: 'test', + }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + return createdRule.id; + }; + + const getAlerts = async () => { + const { + hits: { hits: alerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + + return alerts; + }; + + afterEach(async () => { + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { + match_all: {}, + }, + conflicts: 'proceed', + }); + await objectRemover.removeAll(); + }); + + it('should bulk disable and untrack', async () => { + const createdRule1 = await createRule(); + const createdRule2 = await createRule(); + + await retry.try(async () => { + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await supertest + .patch(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_disable`) + .set('kbn-xsrf', 'foo') + .send({ + ids: [createdRule1, createdRule2], + untrack: true, + }) + .expect(200); + + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((untrackedAlert: any) => { + expect(untrackedAlert._source[ALERT_STATUS]).eql('untracked'); + }); + }); + + it('should bulk disable and not untrack if untrack is false', async () => { + const createdRule1 = await createRule(); + const createdRule2 = await createRule(); + + await retry.try(async () => { + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await supertest + .patch(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_disable`) + .set('kbn-xsrf', 'foo') + .send({ + ids: [createdRule1, createdRule2], + untrack: false, + }) + .expect(200); + + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts index 15084a47f4d86..86c239250d109 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -23,6 +23,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./bulk_disable')); loadTestFile(require.resolve('./capped_action_type')); loadTestFile(require.resolve('./scheduled_task_id')); loadTestFile(require.resolve('./run_soon')); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index c7dc220e96723..0d7539c566e7e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -200,6 +200,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await actionsMenuItemElem.at(1)?.click(); + await testSubjects.click('confirmModalConfirmButton'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async () => { expect(await actionsDropdown.getVisibleText()).to.eql('Disabled'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts index 1a169c3bd69bf..7838ec24c50a3 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts @@ -17,13 +17,15 @@ import { } from '../../../lib/alert_api_actions'; import { ObjectRemover } from '../../../lib/object_remover'; import { generateUniqueKey } from '../../../lib/get_test_data'; +import { getTestAlertData } from '../../../lib/get_test_data'; -export default ({ getPageObjects, getService }: FtrProviderContext) => { +export default ({ getPageObjects, getPageObject, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const find = getService('find'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const supertest = getService('supertest'); const retry = getService('retry'); + const header = getPageObject('header'); const objectRemover = new ObjectRemover(supertest); async function refreshAlertsList() { @@ -39,6 +41,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('rulesTab'); } + const getAlertSummary = async (ruleId: string) => { + const { body: summary } = await supertest + .get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`) + .expect(200); + return summary; + }; + describe('rules list', function () { const assertRulesLength = async (length: number) => { return await retry.try(async () => { @@ -185,6 +194,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('disableButton'); + await testSubjects.click('confirmModalConfirmButton'); + await refreshAlertsList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); @@ -195,12 +206,93 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); + it('should untrack disable rule if untrack switch is true', async () => { + const { body: createdRule } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.always-firing', + schedule: { interval: '24h' }, + params: { + instances: [{ id: 'alert-id' }], + }, + }) + ) + .expect(200); + + objectRemover.add(createdRule.id, 'alert', 'alerts'); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(Object.keys(alertInstances).length).to.eql(1); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + + await refreshAlertsList(); + await pageObjects.triggersActionsUI.searchAlerts(createdRule.name); + + await testSubjects.click('collapsedItemActions'); + + await testSubjects.click('disableButton'); + + await testSubjects.click('untrackAlertsModalSwitch'); + + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(alertInstances['alert-id'].tracked).to.eql(false); + }); + }); + + it('should not untrack disable rule if untrack switch if false', async () => { + const { body: createdRule } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.always-firing', + schedule: { interval: '24h' }, + params: { + instances: [{ id: 'alert-id' }], + }, + }) + ) + .expect(200); + + objectRemover.add(createdRule.id, 'alert', 'alerts'); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(Object.keys(alertInstances).length).to.eql(1); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + + await refreshAlertsList(); + await pageObjects.triggersActionsUI.searchAlerts(createdRule.name); + + await testSubjects.click('collapsedItemActions'); + + await testSubjects.click('disableButton'); + + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + }); + it('should re-enable single alert', async () => { const createdAlert = await createAlert({ supertest, objectRemover, }); - await disableAlert({ supertest, alertId: createdAlert.id }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -212,8 +304,25 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('disableButton'); - await refreshAlertsList(); - await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( + createdAlert.name, + 'statusDropdown', + 'disabled' + ); + + await testSubjects.click('collapsedItemActions'); + + await retry.waitForWithTimeout('disable button to show up', 30000, async () => { + return await testSubjects.isDisplayed('disableButton'); + }); + + await testSubjects.click('disableButton'); + + await header.waitUntilLoadingHasFinished(); await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( createdAlert.name, @@ -255,6 +364,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('bulkAction'); await testSubjects.click('bulkDisable'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + await retry.try(async () => { const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql('Disabled 1 rule'); diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index e9b87e203fad8..ca1a3f5c622df 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -270,6 +270,10 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await testSubjects.existOrFail('rulesList'); await observability.alerts.rulesPage.clickRuleStatusDropDownMenu(); await observability.alerts.rulesPage.clickDisableFromDropDownMenu(); + + await testSubjects.click('confirmModalConfirmButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('The rule to be disabled', async () => { const tableRows = await find.allByCssSelector('.euiTableRow'); const rows = await getRulesList(tableRows); diff --git a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts index d664d36e99f1e..2ebb4395bf317 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts @@ -22,6 +22,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlCommonPage = getPageObject('svlCommonPage'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const svlTriggersActionsUI = getPageObject('svlTriggersActionsUI'); + const header = getPageObject('header'); const svlObltNavigation = getService('svlObltNavigation'); const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); @@ -300,6 +301,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); await testSubjects.click('disableButton'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + await refreshRulesList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); @@ -387,6 +392,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('bulkAction'); await testSubjects.click('bulkDisable'); + await testSubjects.click('confirmModalConfirmButton'); + await header.waitUntilLoadingHasFinished(); + await retry.try(async () => { const resultToast = await toasts.getToastElement(1); const toastText = await resultToast.getVisibleText();