From c66a86b07ebf267aaeed5304bd091ba87443fae2 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Sat, 14 Oct 2023 02:22:25 +0900 Subject: [PATCH] [RAM] Improve rule interval circuit breaker error message (#168173) ## Summary Improve rule interval circuit breaker error message to the following endpoints: - create - update - bulk edit - bulk enable ### Bulk modification ![Screenshot from 2023-10-06 10-11-24](https://github.com/elastic/kibana/assets/74562234/11271221-4d92-41a4-9c0a-f2f8972c452e) ### Modifying a single rule ![Screenshot from 2023-10-06 10-12-16](https://github.com/elastic/kibana/assets/74562234/4ad5f482-6b68-4eef-8989-3f0013c218b2) ### 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_status_dropdown_sandbox.tsx | 18 ++- x-pack/plugins/alerting/common/index.ts | 1 + ...rule_circuit_breaker_error_message.test.ts | 70 +++++++++ .../rule_circuit_breaker_error_message.ts | 136 ++++++++++++++++++ .../rule/methods/bulk_edit/bulk_edit_rules.ts | 31 ++-- .../rule/methods/create/create_rule.ts | 29 ++-- .../get_schedule_frequency.test.ts | 42 +++--- .../get_schedule_frequency.ts | 15 +- .../methods/get_schedule_frequency/index.ts | 2 + .../rules_client/methods/bulk_enable.ts | 18 ++- .../server/rules_client/methods/enable.ts | 22 ++- .../server/rules_client/methods/update.ts | 38 +++-- .../toast_with_circuit_breaker_content.tsx | 50 +++++++ .../components/rule_status_panel.test.tsx | 6 +- .../components/rule_status_panel.tsx | 8 +- .../sections/rule_form/rule_add.tsx | 25 +++- .../sections/rule_form/rule_edit.test.tsx | 6 +- .../sections/rule_form/rule_edit.tsx | 25 +++- .../components/rule_status_dropdown.test.tsx | 13 ++ .../components/rule_status_dropdown.tsx | 37 ++++- .../rules_list/components/rules_list.tsx | 29 +++- .../components/rules_list_table.tsx | 9 +- .../bulk_edit_with_circuit_breaker.ts | 2 +- .../bulk_enable_with_circuit_breaker.ts | 2 +- .../create_with_circuit_breaker.ts | 17 ++- .../enable_with_circuit_breaker.ts | 2 +- .../update_with_circuit_breaker.ts | 2 +- 27 files changed, 536 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.test.ts create mode 100644 x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/toast_with_circuit_breaker_content.tsx diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/rule_status_dropdown_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/rule_status_dropdown_sandbox.tsx index b1b0644f5dc10..982d55926d96a 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/components/rule_status_dropdown_sandbox.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/components/rule_status_dropdown_sandbox.tsx @@ -18,18 +18,24 @@ export const RuleStatusDropdownSandbox = ({ triggersActionsUi }: SandboxProps) = const [isSnoozedUntil, setIsSnoozedUntil] = useState(null); const [muteAll, setMuteAll] = useState(false); + const onEnableRule: any = async () => { + setEnabled(true); + setMuteAll(false); + setIsSnoozedUntil(null); + }; + + const onDisableRule: any = async () => { + setEnabled(false); + }; + return triggersActionsUi.getRuleStatusDropdown({ rule: { enabled, isSnoozedUntil, muteAll, }, - enableRule: async () => { - setEnabled(true); - setMuteAll(false); - setIsSnoozedUntil(null); - }, - disableRule: async () => setEnabled(false), + enableRule: onEnableRule, + disableRule: onDisableRule, snoozeRule: async (schedule) => { if (schedule.duration === -1) { setIsSnoozedUntil(null); diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index e2e9e477cc4cc..c1b5be4d518a4 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -37,6 +37,7 @@ export * from './rrule_type'; export * from './rule_tags_aggregation'; export * from './iso_weekdays'; export * from './saved_objects/rules/mappings'; +export * from './rule_circuit_breaker_error_message'; export type { MaintenanceWindowModificationMetadata, diff --git a/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.test.ts b/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.test.ts new file mode 100644 index 0000000000000..bb89ebad61af6 --- /dev/null +++ b/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { + getRuleCircuitBreakerErrorMessage, + parseRuleCircuitBreakerErrorMessage, +} from './rule_circuit_breaker_error_message'; + +describe('getRuleCircuitBreakerErrorMessage', () => { + it('should return the correct message', () => { + expect( + getRuleCircuitBreakerErrorMessage({ + name: 'test rule', + action: 'create', + interval: 5, + intervalAvailable: 4, + }) + ).toMatchInlineSnapshot( + `"Error validating circuit breaker - Rule 'test rule' cannot be created. The maximum number of runs per minute would be exceeded. - The rule has 5 runs per minute; there are only 4 runs per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals."` + ); + + expect( + getRuleCircuitBreakerErrorMessage({ + name: 'test rule', + action: 'update', + interval: 1, + intervalAvailable: 1, + }) + ).toMatchInlineSnapshot( + `"Error validating circuit breaker - Rule 'test rule' cannot be updated. The maximum number of runs per minute would be exceeded. - The rule has 1 run per minute; there is only 1 run per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals."` + ); + + expect( + getRuleCircuitBreakerErrorMessage({ + name: 'test rule', + action: 'bulkEdit', + interval: 1, + intervalAvailable: 1, + rules: 5, + }) + ).toMatchInlineSnapshot( + `"Error validating circuit breaker - Rules cannot be bulk edited. The maximum number of runs per minute would be exceeded. - The rules have 1 run per minute; there is only 1 run per minute available. Before you can modify these rules, you must disable other rules or change their check intervals so they run less frequently."` + ); + }); + + it('should parse the error message', () => { + const message = getRuleCircuitBreakerErrorMessage({ + name: 'test rule', + action: 'create', + interval: 5, + intervalAvailable: 4, + }); + + const parsedMessage = parseRuleCircuitBreakerErrorMessage(message); + + expect(parsedMessage.summary).toContain("Rule 'test rule' cannot be created"); + expect(parsedMessage.details).toContain('The rule has 5 runs per minute'); + }); + + it('should passthrough the message if it is not related to circuit breakers', () => { + const parsedMessage = parseRuleCircuitBreakerErrorMessage('random message'); + + expect(parsedMessage.summary).toEqual('random message'); + expect(parsedMessage.details).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.ts b/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.ts new file mode 100644 index 0000000000000..68eea28cdeba7 --- /dev/null +++ b/x-pack/plugins/alerting/common/rule_circuit_breaker_error_message.ts @@ -0,0 +1,136 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +const errorMessageHeader = 'Error validating circuit breaker'; + +const getCreateRuleErrorSummary = (name: string) => { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.createSummary', { + defaultMessage: `Rule '{name}' cannot be created. The maximum number of runs per minute would be exceeded.`, + values: { + name, + }, + }); +}; + +const getUpdateRuleErrorSummary = (name: string) => { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.updateSummary', { + defaultMessage: `Rule '{name}' cannot be updated. The maximum number of runs per minute would be exceeded.`, + values: { + name, + }, + }); +}; + +const getEnableRuleErrorSummary = (name: string) => { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.enableSummary', { + defaultMessage: `Rule '{name}' cannot be enabled. The maximum number of runs per minute would be exceeded.`, + values: { + name, + }, + }); +}; + +const getBulkEditRuleErrorSummary = () => { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.bulkEditSummary', { + defaultMessage: `Rules cannot be bulk edited. The maximum number of runs per minute would be exceeded.`, + }); +}; + +const getBulkEnableRuleErrorSummary = () => { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.bulkEnableSummary', { + defaultMessage: `Rules cannot be bulk enabled. The maximum number of runs per minute would be exceeded.`, + }); +}; + +const getRuleCircuitBreakerErrorDetail = ({ + interval, + intervalAvailable, + rules, +}: { + interval: number; + intervalAvailable: number; + rules: number; +}) => { + if (rules === 1) { + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.ruleDetail', { + defaultMessage: `The rule has {interval, plural, one {{interval} run} other {{interval} runs}} per minute; there {intervalAvailable, plural, one {is only {intervalAvailable} run} other {are only {intervalAvailable} runs}} per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals.`, + values: { + interval, + intervalAvailable, + }, + }); + } + return i18n.translate('xpack.alerting.ruleCircuitBreaker.error.multipleRuleDetail', { + defaultMessage: `The rules have {interval, plural, one {{interval} run} other {{interval} runs}} per minute; there {intervalAvailable, plural, one {is only {intervalAvailable} run} other {are only {intervalAvailable} runs}} per minute available. Before you can modify these rules, you must disable other rules or change their check intervals so they run less frequently.`, + values: { + interval, + intervalAvailable, + }, + }); +}; + +export const getRuleCircuitBreakerErrorMessage = ({ + name = '', + interval, + intervalAvailable, + action, + rules = 1, +}: { + name?: string; + interval: number; + intervalAvailable: number; + action: 'update' | 'create' | 'enable' | 'bulkEdit' | 'bulkEnable'; + rules?: number; +}) => { + let errorMessageSummary: string; + + switch (action) { + case 'update': + errorMessageSummary = getUpdateRuleErrorSummary(name); + break; + case 'create': + errorMessageSummary = getCreateRuleErrorSummary(name); + break; + case 'enable': + errorMessageSummary = getEnableRuleErrorSummary(name); + break; + case 'bulkEdit': + errorMessageSummary = getBulkEditRuleErrorSummary(); + break; + case 'bulkEnable': + errorMessageSummary = getBulkEnableRuleErrorSummary(); + break; + } + + return `Error validating circuit breaker - ${errorMessageSummary} - ${getRuleCircuitBreakerErrorDetail( + { + interval, + intervalAvailable, + rules, + } + )}`; +}; + +export const parseRuleCircuitBreakerErrorMessage = ( + message: string +): { + summary: string; + details?: string; +} => { + if (!message.includes(errorMessageHeader)) { + return { + summary: message, + }; + } + const segments = message.split(' - '); + return { + summary: segments[1], + details: segments[2], + }; +}; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts index d76162696ead2..5bc625f5592b3 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts @@ -25,7 +25,7 @@ import { convertRuleIdsToKueryNode, } from '../../../../lib'; import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; -import { parseDuration } from '../../../../../common/parse_duration'; +import { parseDuration, getRuleCircuitBreakerErrorMessage } from '../../../../../common'; import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; import { @@ -77,7 +77,7 @@ import { transformRuleDomainToRuleAttributes, transformRuleDomainToRule, } from '../../transforms'; -import { validateScheduleLimit } from '../get_schedule_frequency'; +import { validateScheduleLimit, ValidateScheduleLimitResult } from '../get_schedule_frequency'; const isValidInterval = (interval: string | undefined): interval is string => { return interval !== undefined; @@ -326,15 +326,16 @@ async function bulkEditRulesOcc( .map((rule) => rule.attributes.schedule?.interval) .filter(isValidInterval); - try { - if (operations.some((operation) => operation.field === 'schedule')) { - await validateScheduleLimit({ - context, - prevInterval, - updatedInterval, - }); - } - } catch (error) { + let validationPayload: ValidateScheduleLimitResult = null; + if (operations.some((operation) => operation.field === 'schedule')) { + validationPayload = await validateScheduleLimit({ + context, + prevInterval, + updatedInterval, + }); + } + + if (validationPayload) { return { apiKeysToInvalidate: Array.from(apiKeysMap.values()) .filter((value) => value.newApiKey) @@ -342,7 +343,13 @@ async function bulkEditRulesOcc( resultSavedObjects: [], rules: [], errors: rules.map((rule) => ({ - message: `Failed to bulk edit rule - ${error.message}`, + message: getRuleCircuitBreakerErrorMessage({ + name: rule.attributes.name || 'n/a', + interval: validationPayload!.interval, + intervalAvailable: validationPayload!.intervalAvailable, + action: 'bulkEdit', + rules: updatedInterval.length, + }), rule: { id: rule.id, name: rule.attributes.name || 'n/a', diff --git a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts index 616a16a8315ed..d774a80ae4ebc 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.ts @@ -8,7 +8,7 @@ import Semver from 'semver'; import Boom from '@hapi/boom'; import { SavedObject, SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; -import { parseDuration } from '../../../../../common/parse_duration'; +import { parseDuration, getRuleCircuitBreakerErrorMessage } from '../../../../../common'; import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; import { validateRuleTypeParams, @@ -36,7 +36,7 @@ import { RuleAttributes } from '../../../../data/rule/types'; import type { CreateRuleData } from './types'; import { createRuleDataSchema } from './schemas'; import { createRuleSavedObject } from '../../../../rules_client/lib'; -import { validateScheduleLimit } from '../get_schedule_frequency'; +import { validateScheduleLimit, ValidateScheduleLimitResult } from '../get_schedule_frequency'; export interface CreateRuleOptions { id?: string; @@ -61,16 +61,29 @@ export async function createRule( try { createRuleDataSchema.validate(data); - if (data.enabled) { - await validateScheduleLimit({ - context, - updatedInterval: data.schedule.interval, - }); - } } catch (error) { throw Boom.badRequest(`Error validating create data - ${error.message}`); } + let validationPayload: ValidateScheduleLimitResult = null; + if (data.enabled) { + validationPayload = await validateScheduleLimit({ + context, + updatedInterval: data.schedule.interval, + }); + } + + if (validationPayload) { + throw Boom.badRequest( + getRuleCircuitBreakerErrorMessage({ + name: data.name, + interval: validationPayload!.interval, + intervalAvailable: validationPayload!.intervalAvailable, + action: 'create', + }) + ); + } + try { await withSpan({ name: 'authorization.ensureAuthorized', type: 'rules' }, () => context.authorization.ensureAuthorized({ diff --git a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.test.ts index d23e4b3a7dd54..c9c890b2ff6ad 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.test.ts @@ -183,53 +183,55 @@ describe('validateScheduleLimit', () => { jest.clearAllMocks(); }); - test('should not throw if the updated interval does not exceed limits', () => { - return expect( - validateScheduleLimit({ + test('should not return anything if the updated interval does not exceed limits', async () => { + expect( + await validateScheduleLimit({ context, updatedInterval: ['1m', '1m'], }) - ).resolves.toBe(undefined); + ).toBeNull(); }); - test('should throw if the updated interval exceeds limits', () => { - return expect( - validateScheduleLimit({ + test('should return interval if the updated interval exceeds limits', async () => { + expect( + await validateScheduleLimit({ context, updatedInterval: ['1m', '1m', '1m', '2m'], }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Run limit reached: The rule has 3.5 runs per minute; there are only 3 runs per minute available."` - ); + ).toEqual({ + interval: 3.5, + intervalAvailable: 3, + }); }); - test('should not throw if previous interval was modified to be under the limit', () => { + test('should not return anything if previous interval was modified to be under the limit', async () => { internalSavedObjectsRepository.find.mockResolvedValue( getMockAggregationResult([{ interval: '1m', count: 6 }]) ); - return expect( - validateScheduleLimit({ + expect( + await validateScheduleLimit({ context, prevInterval: ['1m', '1m'], updatedInterval: ['2m', '2m'], }) - ).resolves.toBe(undefined); + ).toBeNull(); }); - test('should throw if the previous interval was modified to exceed the limit', () => { + test('should return interval if the previous interval was modified to exceed the limit', async () => { internalSavedObjectsRepository.find.mockResolvedValue( getMockAggregationResult([{ interval: '1m', count: 5 }]) ); - return expect( - validateScheduleLimit({ + expect( + await validateScheduleLimit({ context, prevInterval: ['1m'], updatedInterval: ['30s'], }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Run limit reached: The rule has 2 runs per minute; there are only 1 runs per minute available."` - ); + ).toEqual({ + interval: 2, + intervalAvailable: 0, + }); }); }); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.ts b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.ts index 254cad93fd341..b670adeccae8a 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/get_schedule_frequency.ts @@ -85,7 +85,11 @@ interface ValidateScheduleLimitParams { updatedInterval: string | string[]; } -export const validateScheduleLimit = async (params: ValidateScheduleLimitParams) => { +export type ValidateScheduleLimitResult = { interval: number; intervalAvailable: number } | null; + +export const validateScheduleLimit = async ( + params: ValidateScheduleLimitParams +): Promise => { const { context, prevInterval = [], updatedInterval = [] } = params; const prevIntervalArray = Array.isArray(prevInterval) ? prevInterval : [prevInterval]; @@ -108,8 +112,11 @@ export const validateScheduleLimit = async (params: ValidateScheduleLimitParams) const computedRemainingSchedulesPerMinute = remainingSchedulesPerMinute + prevSchedulePerMinute; if (computedRemainingSchedulesPerMinute < updatedSchedulesPerMinute) { - throw new Error( - `Run limit reached: The rule has ${updatedSchedulesPerMinute} runs per minute; there are only ${computedRemainingSchedulesPerMinute} runs per minute available.` - ); + return { + interval: updatedSchedulesPerMinute, + intervalAvailable: remainingSchedulesPerMinute, + }; } + + return null; }; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/index.ts index e39a1cd8a671c..5b26d6a9b9a77 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/index.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/get_schedule_frequency/index.ts @@ -7,4 +7,6 @@ export type { GetScheduleFrequencyResult } from './types'; +export type { ValidateScheduleLimitResult } from './get_schedule_frequency'; + export { getScheduleFrequency, validateScheduleLimit } from './get_schedule_frequency'; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts index fda778e6b11af..cac39ccb367d4 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts @@ -19,6 +19,7 @@ import { buildKueryNodeFilter, getAndValidateCommonBulkOptions, } from '../common'; +import { getRuleCircuitBreakerErrorMessage } from '../../../common'; import { getAuthorizationFilter, checkAuthorizationAndGetTotal, @@ -143,13 +144,18 @@ const bulkEnableRulesWithOCC = async ( .filter((rule) => !rule.attributes.enabled) .map((rule) => rule.attributes.schedule?.interval); - try { - await validateScheduleLimit({ - context, - updatedInterval, + const validationPayload = await validateScheduleLimit({ + context, + updatedInterval, + }); + + if (validationPayload) { + scheduleValidationError = getRuleCircuitBreakerErrorMessage({ + interval: validationPayload.interval, + intervalAvailable: validationPayload.intervalAvailable, + action: 'bulkEnable', + rules: updatedInterval.length, }); - } catch (error) { - scheduleValidationError = `Error validating enable rule data - ${error.message}`; } await pMap(rulesFinderRules, async (rule) => { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts index 97e677a0c28cc..53df42f012ad8 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts @@ -15,6 +15,7 @@ import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { RulesClientContext } from '../types'; import { updateMeta, createNewAPIKeySet, scheduleTask, migrateLegacyActions } from '../lib'; import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency'; +import { getRuleCircuitBreakerErrorMessage } from '../../../common'; export async function enable(context: RulesClientContext, { id }: { id: string }): Promise { return await retryIfConflicts( @@ -48,13 +49,20 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string } references = alert.references; } - try { - await validateScheduleLimit({ - context, - updatedInterval: attributes.schedule.interval, - }); - } catch (error) { - throw Boom.badRequest(`Error validating enable rule data - ${error.message}`); + const validationPayload = await validateScheduleLimit({ + context, + updatedInterval: attributes.schedule.interval, + }); + + if (validationPayload) { + throw Boom.badRequest( + getRuleCircuitBreakerErrorMessage({ + name: attributes.name, + interval: validationPayload.interval, + intervalAvailable: validationPayload.intervalAvailable, + action: 'enable', + }) + ); } try { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index 925f128f0b8b3..e302b02a0e163 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -17,7 +17,7 @@ import { } from '../../types'; import { validateRuleTypeParams, getRuleNotifyWhenType } from '../../lib'; import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; -import { parseDuration } from '../../../common/parse_duration'; +import { parseDuration, getRuleCircuitBreakerErrorMessage } from '../../../common'; import { retryIfConflicts } from '../../lib/retry_if_conflicts'; import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; @@ -33,7 +33,10 @@ import { createNewAPIKeySet, migrateLegacyActions, } from '../lib'; -import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency'; +import { + validateScheduleLimit, + ValidateScheduleLimitResult, +} from '../../application/rule/methods/get_schedule_frequency'; type ShouldIncrementRevision = (params?: RuleTypeParams) => boolean; @@ -90,18 +93,27 @@ async function updateWithOCC( } const { - attributes: { enabled, schedule }, + attributes: { enabled, schedule, name }, } = alertSavedObject; - try { - if (enabled && schedule.interval !== data.schedule.interval) { - await validateScheduleLimit({ - context, - prevInterval: alertSavedObject.attributes.schedule?.interval, - updatedInterval: data.schedule.interval, - }); - } - } catch (error) { - throw Boom.badRequest(`Error validating update data - ${error.message}`); + + let validationPayload: ValidateScheduleLimitResult = null; + if (enabled && schedule.interval !== data.schedule.interval) { + validationPayload = await validateScheduleLimit({ + context, + prevInterval: alertSavedObject.attributes.schedule?.interval, + updatedInterval: data.schedule.interval, + }); + } + + if (validationPayload) { + throw Boom.badRequest( + getRuleCircuitBreakerErrorMessage({ + name, + interval: validationPayload.interval, + intervalAvailable: validationPayload.intervalAvailable, + action: 'update', + }) + ); } try { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/toast_with_circuit_breaker_content.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/toast_with_circuit_breaker_content.tsx new file mode 100644 index 0000000000000..76149e7eef70a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/toast_with_circuit_breaker_content.tsx @@ -0,0 +1,50 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState, useCallback } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; + +const seeFullErrorMessage = i18n.translate( + 'xpack.triggersActionsUI.components.toastWithCircuitBreaker.seeFullError', + { + defaultMessage: 'See full error', + } +); + +const hideFullErrorMessage = i18n.translate( + 'xpack.triggersActionsUI.components.toastWithCircuitBreaker.hideFullError', + { + defaultMessage: 'Hide full error', + } +); + +export const ToastWithCircuitBreakerContent: React.FC = ({ children }) => { + const [showDetails, setShowDetails] = useState(false); + + const onToggleShowDetails = useCallback(() => { + setShowDetails((prev) => !prev); + }, []); + + return ( + <> + {showDetails && ( + <> + {children} + + + )} + + + + {showDetails ? hideFullErrorMessage : seeFullErrorMessage} + + + + + ); +}; 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 de3a9baf6da8c..889f1269a3d2b 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 @@ -51,7 +51,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ })); const mockAPIs = { - bulkEnableRules: jest.fn(), + bulkEnableRules: jest.fn().mockResolvedValue({ errors: [] }), bulkDisableRules: jest.fn(), snoozeRule: jest.fn(), unsnoozeRule: jest.fn(), @@ -170,7 +170,6 @@ describe('rule status panel', () => { it('should enable the rule when picking enable in the dropdown', async () => { const rule = mockRule({ enabled: false }); - const bulkEnableRules = jest.fn(); const wrapper = mountWithIntl( { healthColor="primary" statusMessage="Ok" requestRefresh={requestRefresh} - bulkEnableRules={bulkEnableRules} /> ); const actionsElem = wrapper @@ -199,7 +197,7 @@ describe('rule status panel', () => { await nextTick(); }); - expect(bulkEnableRules).toHaveBeenCalledTimes(1); + expect(mockAPIs.bulkEnableRules).toHaveBeenCalledTimes(1); }); it('if rule is already enabled should do nothing when picking enable in the dropdown', async () => { 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 7167ad7f9b337..a7b87cc722530 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 @@ -126,12 +126,8 @@ export const RuleStatusPanel: React.FC = ({ { - await bulkDisableRules({ ids: [rule.id] }); - }} - enableRule={async () => { - await bulkEnableRules({ ids: [rule.id] }); - }} + disableRule={() => bulkDisableRules({ ids: [rule.id] })} + enableRule={() => bulkEnableRules({ ids: [rule.id] })} snoozeRule={async () => {}} unsnoozeRule={async () => {}} rule={rule} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index dede9c80d87c8..de2eb91b74c84 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -10,6 +10,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { Rule, RuleTypeParams, @@ -38,6 +40,14 @@ import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; import { DEFAULT_RULE_INTERVAL } from '../../constants'; import { triggersActionsUiConfig } from '../../../common/lib/config_api'; import { getInitialInterval } from './get_initial_interval'; +import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content'; + +const defaultCreateRuleErrorMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText', + { + defaultMessage: 'Cannot create rule.', + } +); const RuleAdd = ({ consumer, @@ -238,12 +248,17 @@ const RuleAdd = ({ ); return newRule; } catch (errorRes) { - toasts.addDanger( - errorRes.body?.message ?? - i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText', { - defaultMessage: 'Cannot create rule.', - }) + const message = parseRuleCircuitBreakerErrorMessage( + errorRes.body?.message || defaultCreateRuleErrorMessage ); + toasts.addDanger({ + title: message.summary, + ...(message.details && { + text: toMountPoint( + {message.details} + ), + }), + }); } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx index 50f8049fc4299..7e937d17f8684 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx @@ -219,9 +219,9 @@ describe('rule_edit', () => { await act(async () => { wrapper.find('[data-test-subj="saveEditedRuleButton"]').last().simulate('click'); }); - expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( - 'Fail message' - ); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Fail message', + }); }); it('should pass in the config into `getRuleErrors`', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index b83d2f068e592..3f1c050fb7e25 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -26,6 +26,8 @@ import { } from '@elastic/eui'; import { cloneDeep, omit } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { Rule, RuleFlyoutCloseReason, @@ -47,6 +49,14 @@ import { ConfirmRuleClose } from './confirm_rule_close'; import { hasRuleChanged } from './has_rule_changed'; import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; import { triggersActionsUiConfig } from '../../../common/lib/config_api'; +import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content'; + +const defaultUpdateRuleErrorMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText', + { + defaultMessage: 'Cannot update rule.', + } +); const cloneAndMigrateRule = (initialRule: Rule) => { const clonedRule = cloneDeep(omit(initialRule, 'notifyWhen', 'throttle')); @@ -181,12 +191,17 @@ export const RuleEdit = ({ ); } } catch (errorRes) { - toasts.addDanger( - errorRes.body?.message ?? - i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText', { - defaultMessage: 'Cannot update rule.', - }) + const message = parseRuleCircuitBreakerErrorMessage( + errorRes.body?.message || defaultUpdateRuleErrorMessage ); + toasts.addDanger({ + title: message.summary, + ...(message.details && { + text: toMountPoint( + {message.details} + ), + }), + }); } setIsSaving(false); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx index 56ca543431185..8bfc131639a30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx @@ -12,6 +12,19 @@ import { RuleStatusDropdown, ComponentOpts } from './rule_status_dropdown'; const NOW_STRING = '2020-03-01T00:00:00.000Z'; const SNOOZE_UNTIL = new Date('2020-03-04T00:00:00.000Z'); +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: () => ({ + services: { + notifications: { + toasts: { + addSuccess: jest.fn(), + addDanger: jest.fn(), + }, + }, + }, + }), +})); + describe('RuleStatusDropdown', () => { const enableRule = jest.fn(); const disableRule = jest.fn(); 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 b5db4dc6ccabd..145fda4e4addd 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 @@ -9,6 +9,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import type { RuleSnooze } from '@kbn/alerting-plugin/common'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { EuiLoadingSpinner, EuiPopover, @@ -20,9 +22,11 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; import { SnoozePanel } from './rule_snooze'; import { isRuleSnoozed } from '../../../lib'; -import { Rule, SnoozeSchedule } from '../../../../types'; +import { Rule, SnoozeSchedule, BulkOperationResponse } from '../../../../types'; +import { ToastWithCircuitBreakerContent } from '../../../components/toast_with_circuit_breaker_content'; export type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; const SNOOZE_END_TIME_FORMAT = 'LL @ LT'; @@ -35,8 +39,8 @@ type DropdownRuleRecord = Pick< export interface ComponentOpts { rule: DropdownRuleRecord; onRuleChanged: () => void; - enableRule: () => Promise; - disableRule: () => Promise; + enableRule: () => Promise; + disableRule: () => Promise; snoozeRule: (snoozeSchedule: SnoozeSchedule) => Promise; unsnoozeRule: (scheduleIds?: string[]) => Promise; isEditable: boolean; @@ -58,6 +62,10 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ const [isEnabled, setIsEnabled] = useState(rule.enabled); const [isSnoozed, setIsSnoozed] = useState(!hideSnoozeOption && isRuleSnoozed(rule)); + const { + notifications: { toasts }, + } = useKibana().services; + useEffect(() => { setIsEnabled(rule.enabled); }, [rule.enabled]); @@ -70,6 +78,25 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ const onClickBadge = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), [setIsPopoverOpen]); const onClosePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + const enableRuleInternal = useCallback(async () => { + const { errors } = await enableRule(); + + if (!errors.length) { + return; + } + + const message = parseRuleCircuitBreakerErrorMessage(errors[0].message); + toasts.addDanger({ + title: message.summary, + ...(message.details && { + text: toMountPoint( + {message.details} + ), + }), + }); + throw new Error(); + }, [enableRule, toasts]); + const onChangeEnabledStatus = useCallback( async (enable: boolean) => { if (rule.enabled === enable) { @@ -78,7 +105,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ setIsUpdating(true); try { if (enable) { - await enableRule(); + await enableRuleInternal(); } else { await disableRule(); } @@ -88,7 +115,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ setIsUpdating(false); } }, - [rule.enabled, isEnabled, onRuleChanged, enableRule, disableRule] + [rule.enabled, isEnabled, onRuleChanged, enableRuleInternal, disableRule] ); const onSnoozeRule = useCallback( 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 9949b51554492..1c40db852e209 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 @@ -11,6 +11,8 @@ import { i18n } from '@kbn/i18n'; import { capitalize, isEmpty, isEqual, sortBy } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import React, { lazy, useEffect, @@ -90,6 +92,7 @@ import { useLoadRuleAggregationsQuery } from '../../../hooks/use_load_rule_aggre import { useLoadRuleTypesQuery } from '../../../hooks/use_load_rule_types_query'; import { useLoadRulesQuery } from '../../../hooks/use_load_rules_query'; import { useLoadConfigQuery } from '../../../hooks/use_load_config_query'; +import { ToastWithCircuitBreakerContent } from '../../../components/toast_with_circuit_breaker_content'; import { getConfirmDeletionButtonText, @@ -550,15 +553,15 @@ export const RulesList = ({ }; const onDisableRule = useCallback( - async (rule: RuleTableItem) => { - await bulkDisableRules({ http, ids: [rule.id] }); + (rule: RuleTableItem) => { + return bulkDisableRules({ http, ids: [rule.id] }); }, [bulkDisableRules] ); const onEnableRule = useCallback( - async (rule: RuleTableItem) => { - await bulkEnableRules({ http, ids: [rule.id] }); + (rule: RuleTableItem) => { + return bulkEnableRules({ http, ids: [rule.id] }); }, [bulkEnableRules] ); @@ -675,7 +678,23 @@ export const RulesList = ({ : await bulkEnableRules({ http, ids: selectedIds }); setIsEnablingRules(false); - showToast({ action: 'ENABLE', errors, total }); + + const circuitBreakerError = errors.find( + (error) => !!parseRuleCircuitBreakerErrorMessage(error.message).details + ); + + if (circuitBreakerError) { + const parsedError = parseRuleCircuitBreakerErrorMessage(circuitBreakerError.message); + toasts.addDanger({ + title: parsedError.summary, + text: toMountPoint( + {parsedError.details} + ), + }); + } else { + showToast({ action: 'ENABLE', errors, total }); + } + await refreshRules(); onClearSelection(); }; 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 3d929cf7bb5b8..458e14b0b0117 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 @@ -50,6 +50,7 @@ import { TriggersActionsUiConfig, RuleTypeRegistryContract, SnoozeSchedule, + BulkOperationResponse, } from '../../../../types'; import { DEFAULT_NUMBER_FORMAT } from '../../../constants'; import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; @@ -125,8 +126,8 @@ export interface RulesListTableProps { onTagClose?: (rule: RuleTableItem) => void; onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void; onRuleChanged: () => Promise; - onEnableRule: (rule: RuleTableItem) => Promise; - onDisableRule: (rule: RuleTableItem) => Promise; + onEnableRule: (rule: RuleTableItem) => Promise; + onDisableRule: (rule: RuleTableItem) => Promise; onSnoozeRule: (rule: RuleTableItem, snoozeSchedule: SnoozeSchedule) => Promise; onUnsnoozeRule: (rule: RuleTableItem, scheduleIds?: string[]) => Promise; onSelectAll: () => void; @@ -193,8 +194,8 @@ export const RulesListTable = (props: RulesListTableProps) => { onManageLicenseClick = EMPTY_HANDLER, onPercentileOptionsChange = EMPTY_HANDLER, onRuleChanged, - onEnableRule = EMPTY_HANDLER, - onDisableRule = EMPTY_HANDLER, + onEnableRule, + onDisableRule, onSnoozeRule = EMPTY_HANDLER, onUnsnoozeRule = EMPTY_HANDLER, onSelectAll = EMPTY_HANDLER, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_edit_with_circuit_breaker.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_edit_with_circuit_breaker.ts index d878eb7404238..a6db48295a90b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_edit_with_circuit_breaker.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_edit_with_circuit_breaker.ts @@ -62,7 +62,7 @@ export default function bulkEditWithCircuitBreakerTests({ getService }: FtrProvi expect(body.errors.length).eql(2); expect(body.errors[0].message).eql( - 'Failed to bulk edit rule - Run limit reached: The rule has 12 runs per minute; there are only 1 runs per minute available.' + 'Error validating circuit breaker - Rules cannot be bulk edited. The maximum number of runs per minute would be exceeded. - The rules have 12 runs per minute; there is only 1 run per minute available. Before you can modify these rules, you must disable other rules or change their check intervals so they run less frequently.' ); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_enable_with_circuit_breaker.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_enable_with_circuit_breaker.ts index d60409223b2b3..e35bdadfaee19 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_enable_with_circuit_breaker.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/bulk_enable_with_circuit_breaker.ts @@ -59,7 +59,7 @@ export default function bulkEnableWithCircuitBreakerTests({ getService }: FtrPro expect(body.errors.length).eql(2); expect(body.errors[0].message).eql( - 'Error validating enable rule data - Run limit reached: The rule has 9 runs per minute; there are only 4 runs per minute available.' + 'Error validating circuit breaker - Rules cannot be bulk enabled. The maximum number of runs per minute would be exceeded. - The rules have 9 runs per minute; there are only 4 runs per minute available. Before you can modify these rules, you must disable other rules or change their check intervals so they run less frequently.' ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/create_with_circuit_breaker.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/create_with_circuit_breaker.ts index bf1a0792a0091..f1aea0fc9ce56 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/create_with_circuit_breaker.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/create_with_circuit_breaker.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib'; @@ -26,11 +27,17 @@ export default function createWithCircuitBreakerTests({ getService }: FtrProvide .expect(200); objectRemover.add('space1', createdRule.id, 'rule', 'alerting'); - await supertest + const { + body: { message }, + } = await supertest .post(`${getUrlPrefix('space1')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData({ schedule: { interval: '10s' } })) .expect(400); + + expect(message).eql( + `Error validating circuit breaker - Rule 'abc' cannot be created. The maximum number of runs per minute would be exceeded. - The rule has 6 runs per minute; there are only 4 runs per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals.` + ); }); it('should prevent rules from being created across spaces', async () => { @@ -41,11 +48,17 @@ export default function createWithCircuitBreakerTests({ getService }: FtrProvide .expect(200); objectRemover.add('space1', createdRule.id, 'rule', 'alerting'); - await supertest + const { + body: { message }, + } = await supertest .post(`${getUrlPrefix('space2')}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData({ schedule: { interval: '10s' } })) .expect(400); + + expect(message).eql( + `Error validating circuit breaker - Rule 'abc' cannot be created. The maximum number of runs per minute would be exceeded. - The rule has 6 runs per minute; there are only 4 runs per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals.` + ); }); it('should allow disabled rules to go over the circuit breaker', async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/enable_with_circuit_breaker.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/enable_with_circuit_breaker.ts index 89a90952ed6a7..eb6691952e9b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/enable_with_circuit_breaker.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/enable_with_circuit_breaker.ts @@ -45,7 +45,7 @@ export default function enableWithCircuitBreakerTests({ getService }: FtrProvide .expect(400); expect(body.message).eql( - 'Error validating enable rule data - Run limit reached: The rule has 12 runs per minute; there are only 4 runs per minute available.' + `Error validating circuit breaker - Rule 'abc' cannot be enabled. The maximum number of runs per minute would be exceeded. - The rule has 12 runs per minute; there are only 4 runs per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals.` ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/update_with_circuit_breaker.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/update_with_circuit_breaker.ts index 2b1b8e749def9..7c2413d5eeb23 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/update_with_circuit_breaker.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/schedule_circuit_breaker/update_with_circuit_breaker.ts @@ -53,7 +53,7 @@ export default function updateWithCircuitBreakerTests({ getService }: FtrProvide .expect(400); expect(body.message).eql( - 'Error validating update data - Run limit reached: The rule has 12 runs per minute; there are only 7 runs per minute available.' + `Error validating circuit breaker - Rule 'abc' cannot be updated. The maximum number of runs per minute would be exceeded. - The rule has 12 runs per minute; there are only 4 runs per minute available. Before you can modify this rule, you must increase its check interval so that it runs less frequently. Alternatively, disable other rules or change their check intervals.` ); });