diff --git a/x-pack/plugins/alerting/common/rule_notify_when_type.ts b/x-pack/plugins/alerting/common/rule_notify_when_type.ts index 4be2e35f2d392..76182636e9f71 100644 --- a/x-pack/plugins/alerting/common/rule_notify_when_type.ts +++ b/x-pack/plugins/alerting/common/rule_notify_when_type.ts @@ -12,6 +12,12 @@ export const RuleNotifyWhenTypeValues = [ ] as const; export type RuleNotifyWhenType = typeof RuleNotifyWhenTypeValues[number]; +export enum RuleNotifyWhen { + CHANGE = 'onActionGroupChange', + ACTIVE = 'onActiveAlert', + THROTTLE = 'onThrottleInterval', +} + export function validateNotifyWhenType(notifyWhen: string) { if (RuleNotifyWhenTypeValues.includes(notifyWhen as RuleNotifyWhenType)) { return; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 48f293fdd0043..b26a50ee2a050 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -30,6 +30,7 @@ export type { RuleParamsAndRefs, GetSummarizedAlertsFnOpts, } from './types'; +export { RuleNotifyWhen } from '../common'; export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; export type { PluginSetupContract, PluginStartContract } from './plugin'; export type { diff --git a/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.test.ts b/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.test.ts index 747f5a8a8cd21..a49067caee967 100644 --- a/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.test.ts @@ -19,6 +19,6 @@ test(`should return 'onThrottleInterval' value if 'notifyWhen' is null and throt expect(getRuleNotifyWhenType(null, '10m')).toEqual('onThrottleInterval'); }); -test(`should return 'onActiveAlert' value if 'notifyWhen' is null and throttle is null`, () => { - expect(getRuleNotifyWhenType(null, null)).toEqual('onActiveAlert'); +test(`should return null value if 'notifyWhen' is null and throttle is null`, () => { + expect(getRuleNotifyWhenType(null, null)).toEqual(null); }); diff --git a/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.ts b/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.ts index 53ccacde75e5c..d9e508a5257c8 100644 --- a/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.ts +++ b/x-pack/plugins/alerting/server/lib/get_rule_notify_when_type.ts @@ -10,8 +10,8 @@ import { RuleNotifyWhenType } from '../types'; export function getRuleNotifyWhenType( notifyWhen: RuleNotifyWhenType | null, throttle: string | null -): RuleNotifyWhenType { +): RuleNotifyWhenType | null { // We allow notifyWhen to be null for backwards compatibility. If it is null, determine its // value based on whether the throttle is set to a value or null - return notifyWhen ? notifyWhen! : throttle ? 'onThrottleInterval' : 'onActiveAlert'; + return notifyWhen ? notifyWhen! : throttle ? 'onThrottleInterval' : null; } diff --git a/x-pack/plugins/alerting/server/routes/clone_rule.ts b/x-pack/plugins/alerting/server/routes/clone_rule.ts index 3e57b4aa1c122..a09098930c1d5 100644 --- a/x-pack/plugins/alerting/server/routes/clone_rule.ts +++ b/x-pack/plugins/alerting/server/routes/clone_rule.ts @@ -69,11 +69,12 @@ const rewriteBodyRes: RewriteResponseCase> = ({ : {}), ...(actions ? { - actions: actions.map(({ group, id, actionTypeId, params }) => ({ + actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({ group, id, params, connector_type_id: actionTypeId, + frequency, })), } : {}), diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index 0b529b35fa466..80bd320e309fd 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -59,11 +59,12 @@ const rewriteBodyRes: RewriteResponseCase> = ({ last_execution_date: executionStatus.lastExecutionDate, last_duration: executionStatus.lastDuration, }, - actions: actions.map(({ group, id, actionTypeId, params }) => ({ + actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({ group, id, params, connector_type_id: actionTypeId, + frequency, })), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), diff --git a/x-pack/plugins/alerting/server/routes/legacy/update.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update.test.ts index 61073c0d4e3bf..756e751e5c6e4 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update.test.ts @@ -13,7 +13,7 @@ import { verifyApiAccess } from '../../lib/license_api_access'; import { mockHandlerArguments } from '../_mock_handler_arguments'; import { rulesClientMock } from '../../rules_client.mock'; import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { RuleNotifyWhenType } from '../../../common'; +import { RuleNotifyWhen } from '../../../common'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; const rulesClient = rulesClientMock.create(); @@ -50,7 +50,7 @@ describe('updateAlertRoute', () => { }, }, ], - notifyWhen: 'onActionGroupChange' as RuleNotifyWhenType, + notifyWhen: RuleNotifyWhen.CHANGE, }; it('updates an alert with proper parameters', async () => { diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts index 47dc3a78c278c..fde5ac590b86e 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts @@ -63,10 +63,7 @@ export const rewriteRule = ({ connector_type_id: actionTypeId, ...(frequency ? { - frequency: { - ...frequency, - notify_when: frequency.notifyWhen, - }, + frequency, } : {}), })), diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts index 48d2253a03fc9..add6ab1c871c2 100644 --- a/x-pack/plugins/alerting/server/routes/resolve_rule.ts +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -54,11 +54,12 @@ const rewriteBodyRes: RewriteResponseCase> last_execution_date: executionStatus.lastExecutionDate, last_duration: executionStatus.lastDuration, }, - actions: actions.map(({ group, id, actionTypeId, params }) => ({ + actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({ group, id, params, connector_type_id: actionTypeId, + frequency, })), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), diff --git a/x-pack/plugins/alerting/server/routes/update_rule.test.ts b/x-pack/plugins/alerting/server/routes/update_rule.test.ts index 853a002ade57c..8c335a9bf9864 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.test.ts @@ -14,7 +14,7 @@ import { mockHandlerArguments } from './_mock_handler_arguments'; import { UpdateOptions } from '../rules_client'; import { rulesClientMock } from '../rules_client.mock'; import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; -import { RuleNotifyWhenType } from '../../common'; +import { RuleNotifyWhen } from '../../common'; import { AsApiContract } from './lib'; import { PartialRule } from '../types'; @@ -50,7 +50,7 @@ describe('updateRuleRoute', () => { }, }, ], - notifyWhen: 'onActionGroupChange' as RuleNotifyWhenType, + notifyWhen: RuleNotifyWhen.CHANGE, }; const updateRequest: AsApiContract['data']> = { diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index c998d5eb50a51..1616cca43e713 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -96,11 +96,12 @@ const rewriteBodyRes: RewriteResponseCase> = ({ : {}), ...(actions ? { - actions: actions.map(({ group, id, actionTypeId, params }) => ({ + actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({ group, id, params, connector_type_id: actionTypeId, + frequency, })), } : {}), diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts index f4ae48e40682c..2924755ec3ba5 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts @@ -19,24 +19,8 @@ export async function validateActions( data: Pick & { actions: NormalizedAlertAction[] } ): Promise { const { actions, notifyWhen, throttle } = data; - const hasNotifyWhen = typeof notifyWhen !== 'undefined'; - const hasThrottle = typeof throttle !== 'undefined'; - let usesRuleLevelFreqParams; - // I removed the below ` && hasThrottle` check temporarily. - // Currently the UI sends "throttle" as undefined but schema converts it to null, so they never become both undefined - // I changed the schema too, but as the UI (and tests) sends "notifyWhen" as string and "throttle" as undefined, they never become both defined. - // We should add it back when the UI is changed (https://github.com/elastic/kibana/issues/143369) - if (hasNotifyWhen) usesRuleLevelFreqParams = true; - else if (!hasNotifyWhen && !hasThrottle) usesRuleLevelFreqParams = false; - else { - throw Boom.badRequest( - i18n.translate('xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined', { - defaultMessage: - 'Rule-level notifyWhen and throttle must both be defined or both be undefined', - }) - ); - } - + const hasRuleLevelNotifyWhen = typeof notifyWhen !== 'undefined'; + const hasRuleLevelThrottle = Boolean(throttle); if (actions.length === 0) { return; } @@ -81,13 +65,13 @@ export async function validateActions( } // check for actions using frequency params if the rule has rule-level frequency params defined - if (usesRuleLevelFreqParams) { + if (hasRuleLevelNotifyWhen || hasRuleLevelThrottle) { const actionsWithFrequency = actions.filter((action) => Boolean(action.frequency)); if (actionsWithFrequency.length) { throw Boom.badRequest( i18n.translate('xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams', { defaultMessage: - 'Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: {groups}', + 'Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: {groups}', values: { groups: actionsWithFrequency.map((a) => a.group).join(', '), }, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index dc97258fa994d..6f3c28437a48e 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -7,7 +7,7 @@ import pMap from 'p-map'; import Boom from '@hapi/boom'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, omit } from 'lodash'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { @@ -540,7 +540,20 @@ async function getUpdatedAttributesFromOperations( // the `isAttributesUpdateSkipped` flag to false. switch (operation.field) { case 'actions': { - await validateActions(context, ruleType, { ...attributes, actions: operation.value }); + try { + await validateActions(context, ruleType, { + ...attributes, + actions: operation.value, + }); + } catch (e) { + // If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params + attributes = await attemptToMigrateLegacyFrequency( + context, + operation, + attributes, + ruleType + ); + } const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( operation, @@ -550,6 +563,18 @@ async function getUpdatedAttributesFromOperations( ruleActions = modifiedAttributes; isAttributesUpdateSkipped = false; } + + // TODO https://github.com/elastic/kibana/issues/148414 + // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies + const firstFrequency = operation.value[0]?.frequency; + if (rule.attributes.consumer === AlertConsumers.SIEM && firstFrequency) { + ruleActions.actions = ruleActions.actions.map((action) => omit(action, 'frequency')); + if (!attributes.notifyWhen) { + attributes.notifyWhen = firstFrequency.notifyWhen; + attributes.throttle = firstFrequency.throttle; + } + } + break; } case 'snoozeSchedule': { @@ -754,3 +779,21 @@ async function saveBulkUpdatedRules( return { result, apiKeysToInvalidate }; } + +async function attemptToMigrateLegacyFrequency( + context: RulesClientContext, + operation: BulkEditOperation, + attributes: SavedObjectsFindResult['attributes'], + ruleType: RuleType +) { + if (operation.field !== 'actions') + throw new Error('Can only perform frequency migration on an action operation'); + // Try to remove the rule-level frequency params, and then validate actions + if (typeof attributes.notifyWhen !== 'undefined') attributes.notifyWhen = undefined; + if (attributes.throttle) attributes.throttle = undefined; + await validateActions(context, ruleType, { + ...attributes, + actions: operation.value, + }); + return attributes; +} diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts index fa7766d9a6740..e8dcefe4a9ef1 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -6,6 +6,8 @@ */ import Semver from 'semver'; import Boom from '@hapi/boom'; +import { omit } from 'lodash'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; import { parseDuration } from '../../../common/parse_duration'; @@ -91,6 +93,17 @@ export async function create( throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } + // TODO https://github.com/elastic/kibana/issues/148414 + // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies + const firstFrequency = data.actions[0]?.frequency; + if (data.consumer === AlertConsumers.SIEM && firstFrequency) { + data.actions = data.actions.map((action) => omit(action, 'frequency')); + if (!data.notifyWhen) { + data.notifyWhen = firstFrequency.notifyWhen; + data.throttle = firstFrequency.throttle; + } + } + await validateActions(context, ruleType, data); await withSpan({ name: 'validateActions', type: 'rules' }, () => validateActions(context, ruleType, data) 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 289f5fe007874..8fa3855b95707 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -6,8 +6,9 @@ */ import Boom from '@hapi/boom'; -import { isEqual } from 'lodash'; +import { isEqual, omit } from 'lodash'; import { SavedObject } from '@kbn/core/server'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { PartialRule, RawRule, @@ -142,6 +143,17 @@ async function updateAlert( ): Promise> { const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId); + // TODO https://github.com/elastic/kibana/issues/148414 + // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies + const firstFrequency = data.actions[0]?.frequency; + if (attributes.consumer === AlertConsumers.SIEM && firstFrequency) { + data.actions = data.actions.map((action) => omit(action, 'frequency')); + if (!attributes.notifyWhen) { + attributes.notifyWhen = firstFrequency.notifyWhen; + attributes.throttle = firstFrequency.throttle; + } + } + // Validate const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate?.params); await validateActions(context, ruleType, data); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 53f4dafa0aeaf..d01911177b2a3 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -16,6 +16,7 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server'; +import { RuleNotifyWhen } from '../../types'; import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -167,6 +168,7 @@ describe('create()', () => { params: { foo: true, }, + frequency: { summary: false, notifyWhen: RuleNotifyWhen.CHANGE, throttle: null }, }, ], }, @@ -444,7 +446,7 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -662,7 +664,7 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -753,7 +755,7 @@ describe('create()', () => { }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - notifyWhen: 'onActiveAlert', + notifyWhen: null, actions: [ { group: 'default', @@ -840,7 +842,7 @@ describe('create()', () => { "alertTypeId": "123", "createdAt": 2019-02-12T21:01:22.479Z, "id": "1", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -945,7 +947,7 @@ describe('create()', () => { }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - notifyWhen: 'onActiveAlert', + notifyWhen: null, actions: [ { group: 'default', @@ -1028,7 +1030,7 @@ describe('create()', () => { "alertTypeId": "123", "createdAt": 2019-02-12T21:01:22.479Z, "id": "1", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -1089,7 +1091,7 @@ describe('create()', () => { snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', - notifyWhen: 'onActiveAlert', + notifyWhen: null, params: { bar: true }, running: false, schedule: { interval: '1m' }, @@ -1123,7 +1125,7 @@ describe('create()', () => { }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - notifyWhen: 'onActiveAlert', + notifyWhen: null, actions: [ { group: 'default', @@ -1160,7 +1162,7 @@ describe('create()', () => { "createdAt": 2019-02-12T21:01:22.479Z, "enabled": false, "id": "1", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -1290,7 +1292,7 @@ describe('create()', () => { snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', - notifyWhen: 'onActiveAlert', + notifyWhen: null, params: { bar: true, parameterThatIsSavedObjectRef: 'soRef_0' }, running: false, schedule: { interval: '1m' }, @@ -1461,7 +1463,7 @@ describe('create()', () => { snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', - notifyWhen: 'onActiveAlert', + notifyWhen: null, params: { bar: true, parameterThatIsSavedObjectRef: 'action_0' }, running: false, schedule: { interval: '1m' }, @@ -1841,7 +1843,7 @@ describe('create()', () => { muteAll: false, snoozeSchedule: [], mutedInstanceIds: [], - notifyWhen: 'onActiveAlert', + notifyWhen: null, actions: [ { group: 'default', @@ -1895,7 +1897,7 @@ describe('create()', () => { }, schedule: { interval: '1m' }, throttle: null, - notifyWhen: 'onActiveAlert', + notifyWhen: null, muteAll: false, snoozeSchedule: [], mutedInstanceIds: [], @@ -1941,7 +1943,7 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", - "notifyWhen": "onActiveAlert", + "notifyWhen": null, "params": Object { "bar": true, }, @@ -2024,7 +2026,7 @@ describe('create()', () => { interval: '1m', }, throttle: null, - notifyWhen: 'onActiveAlert', + notifyWhen: null, params: { bar: true, risk_score: 42, @@ -2420,7 +2422,7 @@ describe('create()', () => { }, schedule: { interval: '1m' }, throttle: null, - notifyWhen: 'onActiveAlert', + notifyWhen: null, muteAll: false, snoozeSchedule: [], mutedInstanceIds: [], @@ -2524,7 +2526,7 @@ describe('create()', () => { }, schedule: { interval: '1m' }, throttle: null, - notifyWhen: 'onActiveAlert', + notifyWhen: null, muteAll: false, snoozeSchedule: [], mutedInstanceIds: [], @@ -2743,7 +2745,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: default, default"` + `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -2773,7 +2775,7 @@ describe('create()', () => { ], }); await expect(rulesClient.create({ data: data2 })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: default"` + `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index ae566c107862a..99a7c32004f46 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -12,7 +12,7 @@ import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mock import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; -import { IntervalSchedule } from '../../types'; +import { IntervalSchedule, RuleNotifyWhen } from '../../types'; import { RecoveredActionGroup } from '../../../common'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; @@ -87,8 +87,6 @@ describe('update()', () => { consumer: 'myApp', scheduledTaskId: 'task-123', params: {}, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -98,6 +96,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: RuleNotifyWhen.CHANGE, + throttle: null, + }, }, ], }, @@ -886,7 +889,7 @@ describe('update()', () => { bar: true, }, throttle: '5m', - notifyWhen: null, + notifyWhen: 'onThrottleInterval', actions: [ { group: 'default', @@ -1249,6 +1252,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], scheduledTaskId: 'task-123', @@ -1292,6 +1300,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], scheduledTaskId: 'task-123', @@ -1314,8 +1327,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -1323,6 +1334,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, @@ -1390,6 +1406,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, { group: 'default', @@ -1398,6 +1419,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, { group: 'default', @@ -1406,6 +1432,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], scheduledTaskId: 'task-123', @@ -1439,8 +1470,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: '5m', - notifyWhen: null, actions: [ { group: 'default', @@ -1448,6 +1477,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '5m', + }, }, { group: 'default', @@ -1455,6 +1489,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '5m', + }, }, { group: 'default', @@ -1462,6 +1501,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '5m', + }, }, ], }, @@ -1488,8 +1532,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -1497,6 +1539,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, @@ -1572,6 +1619,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], scheduledTaskId: taskId, @@ -1603,8 +1655,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -1612,6 +1662,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, @@ -1635,8 +1690,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -1644,6 +1697,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, @@ -1698,7 +1756,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: default, default"` + `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default, default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -1739,7 +1797,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot specify per-action frequency params when notify_when and throttle are defined at the rule level: default"` + `"Cannot specify per-action frequency params when notify_when or throttle are defined at the rule level: default"` ); expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); @@ -1847,8 +1905,6 @@ describe('update()', () => { params: { bar: true, }, - throttle: null, - notifyWhen: null, actions: [ { group: 'default', @@ -1856,6 +1912,11 @@ describe('update()', () => { params: { foo: true, }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index cefbc371e156d..ecf11fa122b36 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -165,19 +165,6 @@ describe('alert_form', () => { const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedRuleTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); - - it('should update throttle value', async () => { - wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="onThrottleInterval"]').simulate('click'); - wrapper.update(); - const newThrottle = 17; - const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); - expect(throttleField.exists()).toBeTruthy(); - throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } }); - const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); - expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); - }); }); describe('alert_form > action_form', () => { @@ -253,6 +240,9 @@ describe('alert_form', () => { setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } + setActionFrequencyProperty={(key: string, value: any, index: number) => + (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) + } actionTypeRegistry={actionTypeRegistry} featureId="alerting" /> diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts index 5234fcfce5cbf..c5f528d1b19b1 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts @@ -51,6 +51,11 @@ describe('BaseRule', () => { params: { message: '{{context.internalShortMessage}}', }, + frequency: { + summary: false, + notifyWhen: 'onThrottleInterval', + throttle: '1d', + }, }, ], alertTypeId: '', @@ -65,8 +70,6 @@ describe('BaseRule', () => { interval: '1m', }, tags: [], - throttle: '1d', - notifyWhen: null, }, }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index 1888265c124f6..3e08d370af56b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -9,6 +9,7 @@ import { Logger, ElasticsearchClient } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { RuleType, + RuleNotifyWhen, RuleExecutorOptions, Alert, RulesClient, @@ -124,6 +125,14 @@ export class BaseRule { return existingRuleData.data[0] as Rule; } + const { + defaultParams: params = {}, + name, + id: alertTypeId, + throttle = '1d', + interval = '1m', + } = this.ruleOptions; + const ruleActions = []; for (const actionData of actions) { const action = await actionsClient.get({ id: actionData.id }); @@ -137,16 +146,14 @@ export class BaseRule { message: '{{context.internalShortMessage}}', ...actionData.config, }, + frequency: { + summary: false, + notifyWhen: RuleNotifyWhen.THROTTLE, + throttle, + }, }); } - const { - defaultParams: params = {}, - name, - id: alertTypeId, - throttle = '1d', - interval = '1m', - } = this.ruleOptions; return await rulesClient.create({ data: { enabled: true, @@ -155,8 +162,6 @@ export class BaseRule { consumer: 'monitoring', name, alertTypeId, - throttle, - notifyWhen: null, schedule: { interval }, actions: ruleActions, }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index 1dca480377c32..f9915777483a8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -6,6 +6,7 @@ */ /* eslint-disable complexity */ +import { omit } from 'lodash'; import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; import type { Toast } from '@kbn/core/public'; @@ -221,6 +222,16 @@ export const useBulkActions = ({ return; } + // TODO: https://github.com/elastic/kibana/issues/148414 + // Strip frequency from actions to comply with Security Solution alert API + if ('actions' in editPayload.value) { + // `actions.frequency` is included in the payload from TriggersActionsUI ActionForm + // but is not included in the type definition for the editPayload, because this type + // definition comes from the Security Solution alert API + // TODO https://github.com/elastic/kibana/issues/148414 fix this discrepancy + editPayload.value.actions = editPayload.value.actions.map((a) => omit(a, 'frequency')); + } + startTransaction({ name: BULK_RULE_ACTIONS.EDIT }); const hideWarningToast = () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 4cc5aed8aab69..339b81f752723 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -13,7 +13,7 @@ import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; import type { ActionVariables } from '@kbn/triggers-actions-ui-plugin/public'; -import type { RuleAction } from '@kbn/alerting-plugin/common'; +import type { RuleAction, RuleActionParam } from '@kbn/alerting-plugin/common'; import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; import type { FieldHook } from '../../../../shared_imports'; import { useFormContext } from '../../../../shared_imports'; @@ -95,8 +95,7 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = ); const setActionParamsProperty = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (key: string, value: any, index: number) => { + (key: string, value: RuleActionParam, index: number) => { // validation is not triggered correctly when actions params updated (more details in https://github.com/elastic/kibana/issues/142217) // wrapping field.setValue in setTimeout fixes the issue above // and triggers validation after params have been updated @@ -128,9 +127,11 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = setActionIdByIndex, setActions: setAlertActionsProperty, setActionParamsProperty, + setActionFrequencyProperty: () => {}, featureId: SecurityConnectorFeatureId, defaultActionMessage: DEFAULT_ACTION_MESSAGE, hideActionHeader: true, + hideNotifyWhen: true, }), [ actions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts index 99b97ea6d89ec..bc2cb970ba7f3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts @@ -70,10 +70,9 @@ export const transformFromAlertThrottle = ( if (legacyRuleActions == null || (rule.actions != null && rule.actions.length > 0)) { if (rule.muteAll || rule.actions.length === 0) { return NOTIFICATION_THROTTLE_NO_ACTIONS; - } else if ( - rule.notifyWhen === 'onActiveAlert' || - (rule.throttle == null && rule.notifyWhen == null) - ) { + } else if (rule.notifyWhen == null) { + return transformFromFirstActionThrottle(rule); + } else if (rule.notifyWhen === 'onActiveAlert') { return NOTIFICATION_THROTTLE_RULE; } else if (rule.throttle == null) { return NOTIFICATION_THROTTLE_NO_ACTIONS; @@ -85,6 +84,13 @@ export const transformFromAlertThrottle = ( } }; +function transformFromFirstActionThrottle(rule: RuleAlertType) { + const frequency = rule.actions[0].frequency ?? null; + if (!frequency || frequency.notifyWhen !== 'onThrottleInterval' || frequency.throttle == null) + return NOTIFICATION_THROTTLE_RULE; + return frequency.throttle; +} + /** * Given a set of actions from an "alerting" Saved Object (SO) this will transform it into a "security_solution" alert action. * If this detects any legacy rule actions it will transform it. If both are sent in which is not typical but possible due to diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 867cb5e8173da..1201ba9b445d6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6713,7 +6713,6 @@ "xpack.alerting.rulesClient.runSoon.disabledRuleError": "Erreur lors de l'exécution de la règle : la règle est désactivée", "xpack.alerting.rulesClient.runSoon.ruleIsRunning": "La règle est déjà en cours d'exécution", "xpack.alerting.rulesClient.snoozeSchedule.limitReached": "La règle ne peut pas avoir plus de 5 planifications en répétition", - "xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined": "Les paramètres de niveau de règle notifyWhen et throttle doivent être tous les deux définis ou non définis", "xpack.alerting.savedObjects.goToRulesButtonText": "Accéder aux règles", "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les alertes sont indisponibles – les informations de licence ne sont pas disponibles actuellement.", "xpack.alerting.taskRunner.warning.maxAlerts": "La règle a dépassé le nombre maximal d'alertes au cours d'une même exécution. Les alertes ont peut-être été manquées et les notifications de récupération retardées", @@ -35083,7 +35082,6 @@ "xpack.triggersActionsUI.ruleDetails.definition": "Définition", "xpack.triggersActionsUI.ruleDetails.description": "Description", "xpack.triggersActionsUI.ruleDetails.noActions": "Aucune action", - "xpack.triggersActionsUI.ruleDetails.notifyWhen": "Notifier", "xpack.triggersActionsUI.ruleDetails.ruleType": "Type de règle", "xpack.triggersActionsUI.ruleDetails.runsEvery": "S'exécute toutes les", "xpack.triggersActionsUI.ruleDetails.securityDetectionRule": "Règle de détection de la sécurité", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8e2a0bfefff6..68b7cbe835e7d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6708,7 +6708,6 @@ "xpack.alerting.rulesClient.runSoon.disabledRuleError": "ルールの実行エラー:ルールが無効です", "xpack.alerting.rulesClient.runSoon.ruleIsRunning": "ルールはすでに実行中です", "xpack.alerting.rulesClient.snoozeSchedule.limitReached": "ルールに含めることができるスヌーズスケジュールは5つまでです", - "xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined": "ルールレベル notifyWhen と調整の両方を定義するか、両方を未定義にする必要があります", "xpack.alerting.savedObjects.goToRulesButtonText": "ルールに移動", "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "アラートを利用できません。現在ライセンス情報が利用できません。", "xpack.alerting.taskRunner.warning.maxAlerts": "ルールは、1回の実行のアラートの最大回数を超えたことを報告しました。アラートを受信できないか、回復通知が遅延する可能性があります", @@ -35052,7 +35051,6 @@ "xpack.triggersActionsUI.ruleDetails.definition": "定義", "xpack.triggersActionsUI.ruleDetails.description": "説明", "xpack.triggersActionsUI.ruleDetails.noActions": "アクションなし", - "xpack.triggersActionsUI.ruleDetails.notifyWhen": "通知", "xpack.triggersActionsUI.ruleDetails.ruleType": "ルールタイプ", "xpack.triggersActionsUI.ruleDetails.runsEvery": "次の間隔で実行", "xpack.triggersActionsUI.ruleDetails.securityDetectionRule": "セキュリティ検出ルール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ea18240f0c2f8..9b94bec9ada38 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6716,7 +6716,6 @@ "xpack.alerting.rulesClient.runSoon.disabledRuleError": "运行规则时出错:规则已禁用", "xpack.alerting.rulesClient.runSoon.ruleIsRunning": "规则已在运行", "xpack.alerting.rulesClient.snoozeSchedule.limitReached": "规则不能具有 5 个以上的暂停计划", - "xpack.alerting.rulesClient.usesValidGlobalFreqParams.oneUndefined": "规则级别 notifyWhen 和限制必须同时进行定义或取消定义", "xpack.alerting.savedObjects.goToRulesButtonText": "前往规则", "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "告警不可用 - 许可信息当前不可用。", "xpack.alerting.taskRunner.warning.maxAlerts": "规则在单次运行中报告了多个最大告警数。可能错过了告警并延迟了恢复通知", @@ -35088,7 +35087,6 @@ "xpack.triggersActionsUI.ruleDetails.definition": "定义", "xpack.triggersActionsUI.ruleDetails.description": "描述", "xpack.triggersActionsUI.ruleDetails.noActions": "无操作", - "xpack.triggersActionsUI.ruleDetails.notifyWhen": "通知", "xpack.triggersActionsUI.ruleDetails.ruleType": "规则类型", "xpack.triggersActionsUI.ruleDetails.runsEvery": "运行间隔", "xpack.triggersActionsUI.ruleDetails.securityDetectionRule": "安全检测规则", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/clone.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/clone.test.ts index 0de3b56f8693d..048b9e926be83 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/clone.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/clone.test.ts @@ -29,7 +29,6 @@ describe('cloneRule', () => { tags: [], name: 'test', rule_type_id: '.index-threshold', - notify_when: 'onActionGroupChange', actions: [ { group: 'threshold met', @@ -38,6 +37,11 @@ describe('cloneRule', () => { level: 'info', message: 'alert ', }, + frequency: { + notifyWhen: 'onActionGroupChange', + throttle: null, + summary: false, + }, connector_type_id: '.server-log', }, ], @@ -59,6 +63,11 @@ describe('cloneRule', () => { "actions": Array [ Object { "actionTypeId": ".server-log", + "frequency": Object { + "notifyWhen": "onActionGroupChange", + "summary": false, + "throttle": null, + }, "group": "threshold met", "id": "1", "params": Object { @@ -83,7 +92,7 @@ describe('cloneRule', () => { "muteAll": undefined, "mutedInstanceIds": undefined, "name": "test", - "notifyWhen": "onActionGroupChange", + "notifyWhen": undefined, "params": Object { "aggType": "count", "groupBy": "all", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts index 9f514893d1836..2a1bf52027180 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts @@ -13,11 +13,13 @@ const transformAction: RewriteRequestCase = ({ id, connector_type_id: actionTypeId, params, + frequency, }) => ({ group, id, params, actionTypeId, + frequency, }); const transformExecutionStatus: RewriteRequestCase = ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts index d3138a115a27b..a16a4c8a64a41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.test.ts @@ -32,7 +32,6 @@ describe('createRule', () => { tags: [], name: 'test', rule_type_id: '.index-threshold', - notify_when: 'onActionGroupChange', actions: [ { group: 'threshold met', @@ -42,6 +41,11 @@ describe('createRule', () => { message: 'alert ', }, connector_type_id: '.server-log', + frequency: { + notifyWhen: 'onActionGroupChange', + throttle: null, + summary: false, + }, }, ], scheduled_task_id: '1', @@ -71,7 +75,6 @@ describe('createRule', () => { enabled: true, throttle: null, ruleTypeId: '.index-threshold', - notifyWhen: 'onActionGroupChange', actions: [ { group: 'threshold met', @@ -82,6 +85,11 @@ describe('createRule', () => { "alert '{{alertName}}' is active for group '{{context.group}}':\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{params.timeWindowSize}}{{params.timeWindowUnit}}\n- Timestamp: {{context.date}}", }, actionTypeId: '.server-log', + frequency: { + notifyWhen: 'onActionGroupChange', + throttle: null, + summary: false, + }, }, ], createdAt: new Date('2021-04-01T21:33:13.247Z'), @@ -101,6 +109,11 @@ describe('createRule', () => { level: 'info', message: 'alert ', }, + frequency: { + notifyWhen: 'onActionGroupChange', + throttle: null, + summary: false, + }, }, ], ruleTypeId: '.index-threshold', @@ -116,7 +129,6 @@ describe('createRule', () => { muteAll: undefined, mutedInstanceIds: undefined, name: 'test', - notifyWhen: 'onActionGroupChange', params: { aggType: 'count', groupBy: 'all', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts index c04b55ff6db49..9d13b5f909c23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts @@ -22,17 +22,20 @@ type RuleCreateBody = Omit< >; const rewriteBodyRequest: RewriteResponseCase = ({ ruleTypeId, - notifyWhen, actions, ...res }): any => ({ ...res, rule_type_id: ruleTypeId, - notify_when: notifyWhen, - actions: actions.map(({ group, id, params }) => ({ + actions: actions.map(({ group, id, params, frequency }) => ({ group, id, params, + frequency: { + notify_when: frequency!.notifyWhen, + throttle: frequency!.throttle, + summary: frequency!.summary, + }, })), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts index 29bf8027bfe47..6b65e6cc64818 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Rule, RuleNotifyWhenType } from '../../../types'; +import { Rule } from '../../../types'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { updateRule } from './update'; @@ -14,7 +14,6 @@ const http = httpServiceMock.createStartContract(); describe('updateRule', () => { test('should call rule update API', async () => { const ruleToUpdate = { - throttle: '1m', consumer: 'alerts', name: 'test', tags: ['foo'], @@ -27,7 +26,6 @@ describe('updateRule', () => { updatedAt: new Date('1970-01-01T00:00:00.000Z'), apiKey: null, apiKeyOwner: null, - notifyWhen: 'onThrottleInterval' as RuleNotifyWhenType, }; const resolvedValue: Rule = { ...ruleToUpdate, @@ -51,7 +49,7 @@ describe('updateRule', () => { Array [ "/api/alerting/rule/12%2F3", Object { - "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}", + "body": "{\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[]}", }, ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts index 76383b405e82a..aefaf9019a967 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts @@ -15,17 +15,17 @@ type RuleUpdatesBody = Pick< RuleUpdates, 'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen' >; -const rewriteBodyRequest: RewriteResponseCase = ({ - notifyWhen, - actions, - ...res -}): any => ({ +const rewriteBodyRequest: RewriteResponseCase = ({ actions, ...res }): any => ({ ...res, - notify_when: notifyWhen, - actions: actions.map(({ group, id, params }) => ({ + actions: actions.map(({ group, id, params, frequency }) => ({ group, id, params, + frequency: { + notify_when: frequency!.notifyWhen, + throttle: frequency!.throttle, + summary: frequency!.summary, + }, })), }); @@ -35,19 +35,14 @@ export async function updateRule({ id, }: { http: HttpSetup; - rule: Pick< - RuleUpdates, - 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen' - >; + rule: Pick; id: string; }): Promise { const res = await http.put>( `${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}`, { body: JSON.stringify( - rewriteBodyRequest( - pick(rule, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) - ) + rewriteBodyRequest(pick(rule, ['name', 'tags', 'schedule', 'params', 'actions'])) ), } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 6beece7c983a6..5c4aaa79f0d95 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -341,6 +341,12 @@ describe('action_form', () => { setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } + setActionFrequencyProperty={(key: string, value: any, index: number) => + (initialAlert.actions[index] = { + ...initialAlert.actions[index], + frequency: { ...initialAlert.actions[index].frequency!, [key]: value }, + }) + } actionTypeRegistry={actionTypeRegistry} setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index f499ab5bfa94f..1edf372334757 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -35,7 +35,7 @@ import { ActionTypeForm } from './action_type_form'; import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; -import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; +import { DEFAULT_FREQUENCY, VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { ConnectorAddModal } from '.'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; @@ -55,6 +55,7 @@ export interface ActionAccordionFormProps { setActionGroupIdByIndex?: (group: string, index: number) => void; setActions: (actions: RuleAction[]) => void; setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void; + setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void; featureId: string; messageVariables?: ActionVariables; setHasActionsDisabled?: (value: boolean) => void; @@ -63,6 +64,7 @@ export interface ActionAccordionFormProps { recoveryActionGroup?: string; isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; hideActionHeader?: boolean; + hideNotifyWhen?: boolean; } interface ActiveActionConnectorState { @@ -77,6 +79,7 @@ export const ActionForm = ({ setActionGroupIdByIndex, setActions, setActionParamsProperty, + setActionFrequencyProperty, featureId, messageVariables, actionGroups, @@ -87,6 +90,7 @@ export const ActionForm = ({ recoveryActionGroup, isActionGroupDisabledForActionType, hideActionHeader, + hideNotifyWhen, }: ActionAccordionFormProps) => { const { http, @@ -210,6 +214,7 @@ export const ActionForm = ({ actionTypeId: actionTypeModel.id, group: defaultActionGroupId, params: {}, + frequency: DEFAULT_FREQUENCY, }); setActionIdByIndex(actionTypeConnectors[0].id, actions.length - 1); } @@ -221,6 +226,7 @@ export const ActionForm = ({ actionTypeId: actionTypeModel.id, group: defaultActionGroupId, params: {}, + frequency: DEFAULT_FREQUENCY, }); setActionIdByIndex(actions.length.toString(), actions.length - 1); setEmptyActionsIds([...emptyActionsIds, actions.length.toString()]); @@ -360,6 +366,7 @@ export const ActionForm = ({ index={index} key={`action-form-action-at-${index}`} setActionParamsProperty={setActionParamsProperty} + setActionFrequencyProperty={setActionFrequencyProperty} actionTypesIndex={actionTypesIndex} connectors={connectors} defaultActionGroupId={defaultActionGroupId} @@ -388,6 +395,7 @@ export const ActionForm = ({ ); setActiveActionItem(undefined); }} + hideNotifyWhen={hideNotifyWhen} /> ); })} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_notify_when.tsx new file mode 100644 index 0000000000000..17113f7e1c40d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_notify_when.tsx @@ -0,0 +1,239 @@ +/* + * 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 { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; +import React, { useState, useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiFormRow, + EuiFieldNumber, + EuiSelect, + EuiText, + EuiSpacer, + EuiSuperSelect, + EuiSuperSelectOption, +} from '@elastic/eui'; +import { some, filter, map } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { getTimeOptions } from '../../../common/lib/get_time_options'; +import { RuleNotifyWhenType, RuleAction } from '../../../types'; +import { DEFAULT_FREQUENCY } from '../../../common/constants'; + +const DEFAULT_NOTIFY_WHEN_VALUE: RuleNotifyWhenType = 'onActionGroupChange'; + +export const NOTIFY_WHEN_OPTIONS: Array> = [ + { + value: 'onActionGroupChange', + inputDisplay: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display', + { + defaultMessage: 'On status changes', + } + ), + 'data-test-subj': 'onActionGroupChange', + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + { + value: 'onActiveAlert', + inputDisplay: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display', + { + defaultMessage: 'On check intervals', + } + ), + 'data-test-subj': 'onActiveAlert', + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, + { + value: 'onThrottleInterval', + inputDisplay: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display', + { + defaultMessage: 'On custom action intervals', + } + ), + 'data-test-subj': 'onThrottleInterval', + dropdownDisplay: ( + <> + + + + +

+ +

+
+ + ), + }, +]; + +interface RuleNotifyWhenProps { + frequency: RuleAction['frequency']; + throttle: number | null; + throttleUnit: string; + onNotifyWhenChange: (notifyWhen: RuleNotifyWhenType) => void; + onThrottleChange: (throttle: number | null, throttleUnit: string) => void; +} + +export const ActionNotifyWhen = ({ + frequency = DEFAULT_FREQUENCY, + throttle, + throttleUnit, + onNotifyWhenChange, + onThrottleChange, +}: RuleNotifyWhenProps) => { + const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState(false); + const [notifyWhenValue, setNotifyWhenValue] = + useState(DEFAULT_NOTIFY_WHEN_VALUE); + + useEffect(() => { + if (frequency.notifyWhen) { + setNotifyWhenValue(frequency.notifyWhen); + } else { + // If 'notifyWhen' is not set, derive value from existence of throttle value + setNotifyWhenValue(frequency.throttle ? RuleNotifyWhen.THROTTLE : RuleNotifyWhen.ACTIVE); + } + }, [frequency]); + + useEffect(() => { + setShowCustomThrottleOpts(notifyWhenValue === 'onThrottleInterval'); + }, [notifyWhenValue]); + + const onNotifyWhenValueChange = useCallback( + (newValue: RuleNotifyWhenType) => { + onNotifyWhenChange(newValue); + setNotifyWhenValue(newValue); + // Calling onNotifyWhenChange and onThrottleChange at the same time interferes with the React state lifecycle + // so wait for onNotifyWhenChange to process before calling onThrottleChange + setTimeout( + () => + onThrottleChange(newValue === 'onThrottleInterval' ? throttle ?? 1 : null, throttleUnit), + 100 + ); + }, + [onNotifyWhenChange, setNotifyWhenValue, onThrottleChange, throttle, throttleUnit] + ); + + const labelForRuleRenotify = [ + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel', { + defaultMessage: 'Notify', + }), + , + ]; + + return ( + <> + + + + {showCustomThrottleOpts && ( + <> + + + + + { + pipe( + some(e.target.value.trim()), + filter((value) => value !== ''), + map((value) => parseInt(value, 10)), + filter((value) => !isNaN(value)), + map((value) => { + onThrottleChange(value, throttleUnit); + }) + ); + }} + /> + + + { + onThrottleChange(throttle, e.target.value); + }} + /> + + + + + )} + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index 6d41f09255599..3d51b918c7e4e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -351,6 +351,7 @@ function getActionTypeForm( onConnectorSelected={onConnectorSelected ?? jest.fn()} defaultActionGroupId={defaultActionGroupId ?? 'default'} setActionParamsProperty={jest.fn()} + setActionFrequencyProperty={jest.fn()} index={index ?? 1} actionTypesIndex={actionTypeIndex ?? actionTypeIndexDefault} actionTypeRegistry={actionTypeRegistry} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index eff0ff126d3bc..4853abb756ab6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Suspense, useEffect, useState } from 'react'; +import React, { Suspense, useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -28,6 +28,10 @@ import { } from '@elastic/eui'; import { isEmpty, partition, some } from 'lodash'; import { ActionVariable, RuleActionParam } from '@kbn/alerting-plugin/common'; +import { + getDurationNumberInItsUnit, + getDurationUnitValue, +} from '@kbn/alerting-plugin/common/parse_duration'; import { betaBadgeProps } from './beta_badge_props'; import { IErrorObject, @@ -44,6 +48,7 @@ import { ActionAccordionFormProps, ActionGroupWithMessageVariables } from './act import { transformActionVariables } from '../../lib/action_variables'; import { useKibana } from '../../../common/lib/kibana'; import { ConnectorsSelection } from './connectors_selection'; +import { ActionNotifyWhen } from './action_notify_when'; export type ActionTypeFormProps = { actionItem: RuleAction; @@ -53,11 +58,13 @@ export type ActionTypeFormProps = { onConnectorSelected: (id: string) => void; onDeleteAction: () => void; setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void; + setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void; actionTypesIndex: ActionTypeIndex; connectors: ActionConnector[]; actionTypeRegistry: ActionTypeRegistryContract; recoveryActionGroup?: string; isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; + hideNotifyWhen?: boolean; } & Pick< ActionAccordionFormProps, | 'defaultActionGroupId' @@ -83,6 +90,7 @@ export const ActionTypeForm = ({ onConnectorSelected, onDeleteAction, setActionParamsProperty, + setActionFrequencyProperty, actionTypesIndex, connectors, defaultActionGroupId, @@ -93,6 +101,7 @@ export const ActionTypeForm = ({ actionTypeRegistry, isActionGroupDisabledForActionType, recoveryActionGroup, + hideNotifyWhen = false, }: ActionTypeFormProps) => { const { application: { capabilities }, @@ -106,6 +115,14 @@ export const ActionTypeForm = ({ const [actionParamsErrors, setActionParamsErrors] = useState<{ errors: IErrorObject }>({ errors: {}, }); + const [actionThrottle, setActionThrottle] = useState( + actionItem.frequency?.throttle + ? getDurationNumberInItsUnit(actionItem.frequency.throttle) + : null + ); + const [actionThrottleUnit, setActionThrottleUnit] = useState( + actionItem.frequency?.throttle ? getDurationUnitValue(actionItem.frequency?.throttle) : 'h' + ); const getDefaultParams = async () => { const connectorType = await actionTypeRegistry.get(actionItem.actionTypeId); @@ -161,6 +178,13 @@ export const ActionTypeForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionItem]); + // useEffect(() => { + // if (!actionItem.frequency) { + // setActionFrequency(DEFAULT_FREQUENCY, index); + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [actionItem.frequency]); + const canSave = hasSaveActionsCapability(capabilities); const actionGroupDisplay = ( @@ -185,6 +209,32 @@ export const ActionTypeForm = ({ ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) : false; + const actionNotifyWhen = ( + { + setActionFrequencyProperty('notifyWhen', notifyWhen, index); + }, + [setActionFrequencyProperty, index] + )} + onThrottleChange={useCallback( + (throttle: number | null, throttleUnit: string) => { + setActionThrottle(throttle); + setActionThrottleUnit(throttleUnit); + setActionFrequencyProperty( + 'throttle', + throttle ? `${throttle}${throttleUnit}` : null, + index + ); + }, + [setActionFrequencyProperty, index] + )} + /> + ); + const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); if (!actionTypeRegistered) return null; @@ -198,10 +248,13 @@ export const ActionTypeForm = ({ connectors.filter((connector) => connector.isPreconfigured) ); + const showSelectActionGroup = actionGroups && selectedActionGroup && setActionGroupIdByIndex; + const accordionContent = checkEnabledResult.isEnabled ? ( <> - {actionGroups && selectedActionGroup && setActionGroupIdByIndex && ( + {showSelectActionGroup && ( <> + @@ -226,10 +279,10 @@ export const ActionTypeForm = ({ setActionGroup(group); }} /> - - )} + {!hideNotifyWhen && actionNotifyWhen} + {(showSelectActionGroup || !hideNotifyWhen) && } { it("renders rule action connector icons for user's selected rule actions", async () => { const wrapper = await setup(); expect(mockedUseFetchRuleActionConnectorsHook).toHaveBeenCalledTimes(1); - expect(wrapper.find('[data-euiicon-type]').length).toBe(2); + expect( + wrapper.find('[data-euiicon-type]').length - wrapper.find('[data-euiicon-type="bell"]').length + ).toBe(2); expect(wrapper.find('[data-euiicon-type="logsApp"]').length).toBe(1); expect(wrapper.find('[data-euiicon-type="logoSlack"]').length).toBe(1); expect(wrapper.find('[data-euiicon-type="index"]').length).toBe(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx index 0f4d1b76dcdcb..6ff952e7f30bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx @@ -15,14 +15,22 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; import { ActionTypeRegistryContract, RuleAction, suspendedComponentWithProps } from '../../../..'; import { useFetchRuleActionConnectors } from '../../../hooks/use_fetch_rule_action_connectors'; +import { NOTIFY_WHEN_OPTIONS } from '../../rule_form/rule_notify_when'; export interface RuleActionsProps { ruleActions: RuleAction[]; actionTypeRegistry: ActionTypeRegistryContract; + legacyNotifyWhen?: RuleNotifyWhenType | null; } -export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProps) { + +export function RuleActions({ + ruleActions, + actionTypeRegistry, + legacyNotifyWhen, +}: RuleActionsProps) { const { isLoadingActionConnectors, actionConnectors } = useFetchRuleActionConnectors({ ruleActions, }); @@ -43,6 +51,12 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp ); } + const getNotifyText = (action: RuleAction) => + (NOTIFY_WHEN_OPTIONS.find((options) => options.value === action.frequency?.notifyWhen) + ?.inputDisplay || + action.frequency?.notifyWhen) ?? + legacyNotifyWhen; + const getActionIconClass = (actionGroupId?: string): IconType | undefined => { const actionGroup = actionTypeRegistry.list().find((group) => group.id === actionGroupId); return typeof actionGroup?.iconClass === 'string' @@ -58,7 +72,8 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp if (isLoadingActionConnectors) return ; return ( - {ruleActions.map(({ actionTypeId, id }, index) => { + {ruleActions.map((action, index) => { + const { actionTypeId, id } = action; const actionName = getActionName(id); return ( @@ -73,8 +88,23 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp > {actionName} + + + + + + + + {String(getNotifyText(action))} + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx index 0a54b40299ffa..6c7f133da753f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx @@ -22,7 +22,6 @@ import { RuleDefinitionProps } from '../../../../types'; import { RuleType, useLoadRuleTypes } from '../../../..'; import { useKibana } from '../../../../common/lib/kibana'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; -import { NOTIFY_WHEN_OPTIONS } from '../../rule_form/rule_notify_when'; import { RuleActions } from './rule_actions'; import { RuleEdit } from '../../rule_form'; @@ -61,10 +60,6 @@ export const RuleDefinition: React.FunctionComponent = ({ values: { numberOfConditions }, }); }; - const getNotifyText = () => - NOTIFY_WHEN_OPTIONS.find((options) => options.value === rule?.notifyWhen)?.inputDisplay || - rule?.notifyWhen; - const canExecuteActions = hasExecuteActionsCapability(capabilities); const canSaveRule = rule && @@ -205,15 +200,6 @@ export const RuleDefinition: React.FunctionComponent = ({ - - - - {i18n.translate('xpack.triggersActionsUI.ruleDetails.notifyWhen', { - defaultMessage: 'Notify', - })} - - - @@ -223,7 +209,11 @@ export const RuleDefinition: React.FunctionComponent = ({ })} - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts index a79e820e2cfd8..4b3920e9a241f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts @@ -10,14 +10,15 @@ import { pick } from 'lodash'; import { RuleTypeParams } from '../../../types'; import { InitialRule } from './rule_reducer'; -const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions', 'notifyWhen']; +const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions']; function getNonNullCompareFields(rule: InitialRule) { - const { name, ruleTypeId, throttle } = rule; + const { name, ruleTypeId, throttle, notifyWhen } = rule; return { ...(!!(name && name.length > 0) ? { name } : {}), ...(!!(ruleTypeId && ruleTypeId.length > 0) ? { ruleTypeId } : {}), ...(!!(throttle && throttle.length > 0) ? { throttle } : {}), + ...(!!(notifyWhen && notifyWhen.length > 0) ? { notifyWhen } : {}), }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index 6735bb5637e74..4b1b4330b9071 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -248,7 +248,9 @@ describe('rule_add', () => { interval: '1h', }, }, - onClose + onClose, + undefined, + 'my-rule-type' ); expect(wrapper.find('input#ruleName').props().value).toBe('Simple status rule'); @@ -259,7 +261,7 @@ describe('rule_add', () => { it('renders rule add flyout with DEFAULT_RULE_INTERVAL if no initialValues specified and no minimumScheduleInterval', async () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({}); - await setup(); + await setup(undefined, undefined, undefined, 'my-rule-type'); expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(1); expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m'); @@ -269,7 +271,7 @@ describe('rule_add', () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ minimumScheduleInterval: { value: '5m', enforce: false }, }); - await setup(); + await setup(undefined, undefined, undefined, 'my-rule-type'); expect(wrapper.find('[data-test-subj="intervalInput"]').first().props().value).toEqual(5); expect(wrapper.find('[data-test-subj="intervalInputUnit"]').first().props().value).toBe('m'); 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 072add4f1f566..76bef4c35788d 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 @@ -65,7 +65,6 @@ const RuleAdd = ({ }, actions: [], tags: [], - notifyWhen: 'onActionGroupChange', ...(initialValues ? initialValues : {}), }; }, [ruleTypeId, consumer, initialValues]); 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 0f5f4af1ea58f..e18d5997e5cf0 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 @@ -7,6 +7,7 @@ import React, { useReducer, useState, useEffect, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import { EuiTitle, EuiFlyoutHeader, @@ -23,7 +24,7 @@ import { EuiLoadingSpinner, EuiIconTip, } from '@elastic/eui'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, omit } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Rule, @@ -32,6 +33,7 @@ import { IErrorObject, RuleType, TriggersActionsUiConfig, + RuleNotifyWhenType, } from '../../../types'; import { RuleForm } from './rule_form'; import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; @@ -45,6 +47,29 @@ import { hasRuleChanged } from './has_rule_changed'; import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; import { triggersActionsUiConfig } from '../../../common/lib/config_api'; +const cloneAndMigrateRule = (initialRule: Rule) => { + const clonedRule = cloneDeep(omit(initialRule, 'notifyWhen', 'throttle')); + + const hasRuleLevelNotifyWhen = Boolean(initialRule.notifyWhen); + const hasRuleLevelThrottle = Boolean(initialRule.throttle); + + if (hasRuleLevelNotifyWhen || hasRuleLevelThrottle) { + const frequency = hasRuleLevelNotifyWhen + ? { + summary: false, + notifyWhen: initialRule.notifyWhen as RuleNotifyWhenType, + throttle: + initialRule.notifyWhen === RuleNotifyWhen.THROTTLE ? initialRule.throttle! : null, + } + : { summary: false, notifyWhen: RuleNotifyWhen.THROTTLE, throttle: initialRule.throttle! }; + clonedRule.actions = clonedRule.actions.map((action) => ({ + ...action, + frequency, + })); + } + return clonedRule; +}; + export const RuleEdit = ({ initialRule, onClose, @@ -57,7 +82,7 @@ export const RuleEdit = ({ }: RuleEditProps) => { const onSaveHandler = onSave ?? reloadRules; const [{ rule }, dispatch] = useReducer(ruleReducer as ConcreteRuleReducer, { - rule: cloneDeep(initialRule), + rule: cloneAndMigrateRule(initialRule), }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index 316bfbec19a54..f0de7e6e4ed60 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -233,7 +233,12 @@ describe('rule_form', () => { describe('rule_form create rule', () => { let wrapper: ReactWrapper; - async function setup(enforceMinimum = false, schedule = '1m', featureId = 'alerting') { + async function setup( + showRulesList = false, + enforceMinimum = false, + schedule = '1m', + featureId = 'alerting' + ) { const mocks = coreMock.createSetup(); const { useLoadRuleTypes } = jest.requireMock('../../hooks/use_load_rule_types'); const ruleTypes: RuleType[] = [ @@ -320,6 +325,7 @@ describe('rule_form', () => { muteAll: false, enabled: false, mutedInstanceIds: [], + ...(!showRulesList ? { ruleTypeId: ruleType.id } : {}), } as unknown as Rule; wrapper = mountWithIntl( @@ -353,20 +359,20 @@ describe('rule_form', () => { }); it('renders registered selected rule type', async () => { - await setup(); + await setup(true); const ruleTypeSelectOptions = wrapper.find('[data-test-subj="my-rule-type-SelectOption"]'); expect(ruleTypeSelectOptions.exists()).toBeTruthy(); }); it('renders minimum schedule interval helper text when enforce = true', async () => { - await setup(true); + await setup(false, true); expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( `Interval must be at least 1 minute.` ); }); it('renders minimum schedule interval helper suggestion when enforce = false and schedule is less than configuration', async () => { - await setup(false, '10s'); + await setup(false, false, '10s'); expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( `Intervals less than 1 minute are not recommended due to performance considerations.` ); @@ -434,7 +440,7 @@ describe('rule_form', () => { }); it('renders uses feature id to load action types', async () => { - await setup(false, '1m', 'anotherFeature'); + await setup(false, false, '1m', 'anotherFeature'); const ruleTypeSelectOptions = wrapper.find( '[data-test-subj=".server-log-anotherFeature-ActionTypeSelectOption"]' ); @@ -442,7 +448,7 @@ describe('rule_form', () => { }); it('renders rule type description', async () => { - await setup(); + await setup(true); wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); expect(ruleDescription.exists()).toBeTruthy(); @@ -450,7 +456,7 @@ describe('rule_form', () => { }); it('renders rule type documentation link', async () => { - await setup(); + await setup(true); wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); expect(ruleDocumentationLink.exists()).toBeTruthy(); @@ -458,7 +464,7 @@ describe('rule_form', () => { }); it('renders rule types disabled by license', async () => { - await setup(); + await setup(true); const actionOption = wrapper.find(`[data-test-subj="disabled-by-license-SelectOption"]`); expect(actionOption.exists()).toBeTruthy(); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index c06c5179acf60..94a7cc5bba513 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -69,7 +69,6 @@ import './rule_form.scss'; import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage } from '../../constants'; import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_enabled'; -import { RuleNotifyWhen } from './rule_notify_when'; import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; @@ -146,12 +145,6 @@ export const RuleForm = ({ ? getDurationUnitValue(rule.schedule.interval) : defaultScheduleIntervalUnit ); - const [ruleThrottle, setRuleThrottle] = useState( - rule.throttle ? getDurationNumberInItsUnit(rule.throttle) : null - ); - const [ruleThrottleUnit, setRuleThrottleUnit] = useState( - rule.throttle ? getDurationUnitValue(rule.throttle) : 'h' - ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); const [availableRuleTypes, setAvailableRuleTypes] = useState< @@ -289,6 +282,13 @@ export const RuleForm = ({ [dispatch] ); + const setActionFrequencyProperty = useCallback( + (key: string, value: RuleActionParam, index: number) => { + dispatch({ command: { type: 'setRuleActionFrequency' }, payload: { key, value, index } }); + }, + [dispatch] + ); + useEffect(() => { const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null; setFilteredRuleTypes( @@ -420,8 +420,8 @@ export const RuleForm = ({ isDisabled={!item.checkEnabledResult.isEnabled} onClick={() => { setRuleProperty('ruleTypeId', item.id); - setActions([]); setRuleTypeModel(item.ruleTypeItem); + setActions([]); setRuleProperty('params', {}); if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); @@ -435,6 +435,58 @@ export const RuleForm = ({ )); + const labelForRuleChecked = [ + i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel', { + defaultMessage: 'Check every', + }), + , + ]; + + const getHelpTextForInterval = () => { + if (!config || !config.minimumScheduleInterval) { + return ''; + } + + // No help text if there is an error + if (errors['schedule.interval'].length > 0) { + return ''; + } + + if (config.minimumScheduleInterval.enforce) { + // Always show help text if minimum is enforced + return i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText', { + defaultMessage: 'Interval must be at least {minimum}.', + values: { + minimum: formatDuration(config.minimumScheduleInterval.value, true), + }, + }); + } else if ( + rule.schedule.interval && + parseDuration(rule.schedule.interval) < parseDuration(config.minimumScheduleInterval.value) + ) { + // Only show help text if current interval is less than suggested + return i18n.translate( + 'xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText', + { + defaultMessage: + 'Intervals less than {minimum} are not recommended due to performance considerations.', + values: { + minimum: formatDuration(config.minimumScheduleInterval.value, true), + }, + } + ); + } else { + return ''; + } + }; + const ruleTypeDetails = ( <> @@ -513,7 +565,7 @@ export const RuleForm = ({ ) : null} + + 0} + error={errors['schedule.interval']} + > + + + 0} + value={ruleInterval || ''} + name="interval" + data-test-subj="intervalInput" + onChange={(e) => { + const value = e.target.value; + if (value === '' || INTEGER_REGEX.test(value)) { + const parsedValue = value === '' ? '' : parseInt(value, 10); + setRuleInterval(parsedValue || undefined); + setScheduleProperty('interval', `${parsedValue}${ruleIntervalUnit}`); + } + }} + /> + + + { + setRuleIntervalUnit(e.target.value); + setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); + }} + data-test-subj="intervalInputUnit" + /> + + + + + {canShowActions && defaultActionGroupId && ruleTypeModel && @@ -543,6 +640,7 @@ export const RuleForm = ({ ) : null} + ) : null} ); - const labelForRuleChecked = ( - <> - {' '} - - - ); - - const getHelpTextForInterval = () => { - if (!config || !config.minimumScheduleInterval) { - return ''; - } - - // No help text if there is an error - if (errors['schedule.interval'].length > 0) { - return ''; - } - - if (config.minimumScheduleInterval.enforce) { - // Always show help text if minimum is enforced - return i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText', { - defaultMessage: 'Interval must be at least {minimum}.', - values: { - minimum: formatDuration(config.minimumScheduleInterval.value, true), - }, - }); - } else if ( - rule.schedule.interval && - parseDuration(rule.schedule.interval) < parseDuration(config.minimumScheduleInterval.value) - ) { - // Only show help text if current interval is less than suggested - return i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText', - { - defaultMessage: - 'Intervals less than {minimum} are not recommended due to performance considerations.', - values: { - minimum: formatDuration(config.minimumScheduleInterval.value, true), - }, - } - ); - } else { - return ''; - } - }; - return ( - + - - - 0} - error={errors['schedule.interval']} - > - - - 0} - value={ruleInterval || ''} - name="interval" - data-test-subj="intervalInput" - onChange={(e) => { - const value = e.target.value; - if (value === '' || INTEGER_REGEX.test(value)) { - const parsedValue = value === '' ? '' : parseInt(value, 10); - setRuleInterval(parsedValue || undefined); - setScheduleProperty('interval', `${parsedValue}${ruleIntervalUnit}`); - } - }} - /> - - - { - setRuleIntervalUnit(e.target.value); - setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); - }} - data-test-subj="intervalInputUnit" - /> - - - - - - { - setRuleProperty('notifyWhen', notifyWhen); - }, - [setRuleProperty] - )} - onThrottleChange={useCallback( - (throttle: number | null, throttleUnit: string) => { - setRuleThrottle(throttle); - setRuleThrottleUnit(throttleUnit); - setRuleProperty('throttle', throttle ? `${throttle}${throttleUnit}` : null); - }, - [setRuleProperty] - )} - /> - - - {ruleTypeModel ? ( <>{ruleTypeDetails} ) : availableRuleTypes.length ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts index 259cb5ed3ecea..9b066bb1060a0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts @@ -186,4 +186,25 @@ describe('rule reducer', () => { ); expect(updatedRule.rule.actions[0].group).toBe('Warning'); }); + + test('if rule action frequency was updated', () => { + initialRule.actions.push({ + id: '', + actionTypeId: 'testId', + group: 'Rule', + params: {}, + }); + const updatedRule = ruleReducer( + { rule: initialRule }, + { + command: { type: 'setRuleActionFrequency' }, + payload: { + key: 'notifyWhen', + value: 'onThrottleInterval', + index: 0, + }, + } + ); + expect(updatedRule.rule.actions[0].frequency?.notifyWhen).toBe('onThrottleInterval'); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts index 7f9950ddd5d0c..55ce45d592d00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts @@ -10,9 +10,10 @@ import { isEqual } from 'lodash'; import { Reducer } from 'react'; import { RuleActionParam, IntervalSchedule } from '@kbn/alerting-plugin/common'; import { Rule, RuleAction } from '../../../types'; +import { DEFAULT_FREQUENCY } from '../../../common/constants'; export type InitialRule = Partial & - Pick; + Pick; interface CommandType< T extends @@ -22,6 +23,7 @@ interface CommandType< | 'setRuleParams' | 'setRuleActionParams' | 'setRuleActionProperty' + | 'setRuleActionFrequency' > { type: T; } @@ -77,7 +79,11 @@ export type RuleReducerAction = } | { command: CommandType<'setRuleActionProperty'>; - payload: RuleActionPayload; + payload: Payload; + } + | { + command: CommandType<'setRuleActionFrequency'>; + payload: Payload; }; export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>; @@ -179,6 +185,36 @@ export const ruleReducer = ( }; } } + case 'setRuleActionFrequency': { + const { key, value, index } = action.payload as Payload< + keyof RuleAction, + SavedObjectAttribute + >; + if ( + index === undefined || + rule.actions[index] == null || + (!!rule.actions[index][key] && isEqual(rule.actions[index][key], value)) + ) { + return state; + } else { + const oldAction = rule.actions.splice(index, 1)[0]; + const updatedAction = { + ...oldAction, + frequency: { + ...(oldAction.frequency ?? DEFAULT_FREQUENCY), + [key]: value, + }, + }; + rule.actions.splice(index, 0, updatedAction); + return { + ...state, + rule: { + ...rule, + actions: [...rule.actions], + }, + }; + } + } case 'setRuleActionProperty': { const { key, value, index } = action.payload as RuleActionPayload; if (index === undefined || isEqual(rule.actions[index][key], value)) { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/action_frequency_types.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/action_frequency_types.ts new file mode 100644 index 0000000000000..7ebfc502ed07d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/action_frequency_types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; + +export const DEFAULT_FREQUENCY = { + notifyWhen: RuleNotifyWhen.CHANGE, + throttle: null, + summary: false, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index 1db1551ecfd38..33d859b378c17 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -8,6 +8,7 @@ export { COMPARATORS, builtInComparators } from './comparators'; export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types'; export { builtInGroupByTypes } from './group_by_types'; +export * from './action_frequency_types'; export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts index fc3dc32483ba4..10089383b1028 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext) => { expect(notifyWhen).to.eql('onActiveAlert'); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: NOTIFICATION_THROTTLE_NO_ACTIONS, @@ -82,10 +82,10 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(true); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and with actions set, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and with actions set, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -102,7 +102,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(true); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { diff --git a/x-pack/test/detection_engine_api_integration/utils/create_rule.ts b/x-pack/test/detection_engine_api_integration/utils/create_rule.ts index 278bf0a92b40f..b110b8afb905d 100644 --- a/x-pack/test/detection_engine_api_integration/utils/create_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/create_rule.ts @@ -59,7 +59,9 @@ export const createRule = async ( } } else if (response.status !== 200) { throw new Error( - `Unexpected non 200 ok when attempting to create a rule: ${JSON.stringify(response.status)}` + `Unexpected non 200 ok when attempting to create a rule: ${JSON.stringify( + response.status + )},${JSON.stringify(response, null, 4)}` ); } else { return response.body; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 042605c0ae585..b788b4f875c6e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -89,10 +89,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alertName = generateUniqueKey(); await rules.common.defineIndexThresholdAlert(alertName); - await testSubjects.click('notifyWhenSelect'); - await testSubjects.click('onThrottleInterval'); - await testSubjects.setValue('throttleInput', '10'); - // filterKuery validation await testSubjects.setValue('filterKuery', 'group:'); const filterKueryInput = await testSubjects.find('filterKuery'); @@ -108,6 +104,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); + await testSubjects.click('notifyWhenSelect'); + await testSubjects.click('onThrottleInterval'); + await testSubjects.setValue('throttleInput', '10'); const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]'); expect(await messageTextArea.getAttribute('value')).to.eql( `alert '{{alertName}}' is active for group '{{context.group}}': @@ -236,7 +235,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.missingOrFail('confirmRuleCloseModal'); await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('intervalInput', '10'); + await testSubjects.setValue('ruleNameInput', 'alertName'); await testSubjects.click('cancelSaveRuleButton'); await testSubjects.existOrFail('confirmRuleCloseModal'); await testSubjects.click('confirmRuleCloseModal > confirmModalCancelButton'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index bb6d8a46988a5..8c336a960638b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -493,6 +493,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { id: action.id, group: 'default', params: { level: 'info', message: 'gfghfhg' }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, @@ -669,6 +674,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { id: action.id, group: 'default', params: { level: 'info', message: 'gfghfhg' }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, }, ], }, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts index 27cd136cb40c7..6b37fa90119ea 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/opsgenie.ts @@ -356,13 +356,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { await rules.common.defineIndexThresholdAlert(alertName); }); - - await rules.common.setNotifyThrottleInput(); }; const selectOpsgenieConnectorInRuleAction = async (name: string) => { await testSubjects.click('.opsgenie-alerting-ActionTypeSelectOption'); await testSubjects.selectValue('comboBoxInput', name); + await rules.common.setNotifyThrottleInput(); }; const createOpsgenieConnector = async (name: string) => { 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 6c5de895ed1fd..806844c71b57d 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 @@ -11,6 +11,7 @@ import { omit, mapValues, range, flatten } from 'lodash'; import moment from 'moment'; import { asyncForEach } from '@kbn/std'; import { alwaysFiringAlertType } from '@kbn/alerting-fixture-plugin/server/plugin'; +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { ObjectRemover } from '../../lib/object_remover'; import { getTestAlertData, getTestActionData } from '../../lib/get_test_data'; @@ -88,6 +89,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { message: 'from alert 1s', level: 'warn', }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, })), params, ...overwrites, @@ -111,6 +117,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { message: 'from alert 1s', level: 'warn', }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, })), params, }); @@ -329,6 +340,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { group: 'threshold met', id: 'my-server-log', params: { level: 'info', message: ' {{context.message}}' }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, }, ], }); @@ -425,6 +441,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { group: 'default', id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, }, ], }); @@ -487,11 +508,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { group: 'default', id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, }, { group: 'other', id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, + frequency: { + summary: false, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '1m', + }, }, ], }); @@ -568,6 +599,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + describe('Edit rule with legacy rule-level notify values', function () { + const testRunUuid = uuid.v4(); + + afterEach(async () => { + await objectRemover.removeAll(); + }); + + it('should convert rule-level params to action-level params and save the alert successfully', async () => { + const connectors = await createConnectors(testRunUuid); + await pageObjects.common.navigateToApp('triggersActions'); + const rule = await createAlwaysFiringRule({ + name: `test-rule-${testRunUuid}`, + schedule: { + interval: '1s', + }, + notify_when: RuleNotifyWhen.THROTTLE, + throttle: '2d', + actions: connectors.map((connector) => ({ + id: connector.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })), + }); + const updatedRuleName = `Changed rule ${rule.name}`; + + // refresh to see rule + await browser.refresh(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + // verify content + await testSubjects.existOrFail('rulesList'); + + // click on first alert + await pageObjects.common.navigateToApp('triggersActions'); + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); + + const editButton = await testSubjects.find('openEditRuleFlyoutButton'); + await editButton.click(); + const notifyWhenSelect = await testSubjects.find('notifyWhenSelect'); + expect(await notifyWhenSelect.getVisibleText()).to.eql('On custom action intervals'); + const throttleInput = await testSubjects.find('throttleInput'); + const throttleUnitInput = await testSubjects.find('throttleUnitInput'); + expect(await throttleInput.getAttribute('value')).to.be('2'); + expect(await throttleUnitInput.getAttribute('value')).to.be('d'); + await testSubjects.setValue('ruleNameInput', updatedRuleName, { + clearWithKeyboard: true, + }); + + await find.clickByCssSelector('[data-test-subj="saveEditedRuleButton"]:not(disabled)'); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Updated '${updatedRuleName}'`); + }); + }); + describe('View In App', function () { const ruleName = uuid.v4(); @@ -921,7 +1010,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }, { schedule: { interval: '1s' }, - throttle: null, } ); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index 636f16c5b98ff..140f85ff068ef 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -49,10 +49,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await alerts.setAlertInterval('11'); }); - it('can set alert throttle interval', async () => { - await alerts.setAlertThrottleInterval('30'); - }); - it('can set alert status number of time', async () => { await alerts.setAlertStatusNumTimes('3'); }); @@ -172,10 +168,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await alerts.setAlertInterval('11'); }); - it('can set alert throttle interval', async () => { - await alerts.setAlertThrottleInterval('30'); - }); - it('can save alert', async () => { await alerts.clickSaveRuleButton(alertId); await alerts.clickSaveAlertsConfirmButton(); diff --git a/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts b/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts index 8dc8a5e06645c..51157932eee3d 100644 --- a/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts +++ b/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts @@ -19,8 +19,6 @@ export function getTestAlertData(overwrites = {}) { rule_type_id: 'test.noop', consumer: 'alerts', schedule: { interval: '1m' }, - throttle: '1m', - notify_when: 'onThrottleInterval', actions: [], params: {}, ...overwrites,