[Alert Summaries] [FE] Move “Notify When” and throttle from rule to action (#145637)

## Summary

Closes #143369 (~blocked by
https://github.com/elastic/kibana/issues/143376~)

This PR updates the Stack Management UI and Observability UI to show
Notify When and Throttle parameters at the **action level** instead of
the **rule level**.

The rule-level Check Every dropdown is moved to the end of the rule,
right above the actions form

The Security Solution UX remains unchanged, as it has a unique way of
displaying action notification frequencies at the rule level. Instead,
the API request has changed so that the selected action frequency will
now be stored in each action's `frequency` param instead of at the rule
level.

In all three UIs, existing rules that have legacy rule-level
`notifyWhen` and `frequency` params will have these parameters
seamlessly migrated to the action level when the user edits a rule.

The Rule Details page is also updated to show Notify frequencies in the
Actions column instead of in the first, rule-level column.

### Rule Details Page update
<img width="781" alt="Screen Shot 2022-11-17 at 4 23 02 PM"
src="https://user-images.githubusercontent.com/1445834/202573067-bc55630d-f767-4a93-8d7c-752748da25c2.png">

### Rule Form update
<img width="605" alt="Screen Shot 2022-11-17 at 4 23 10 PM"
src="https://user-images.githubusercontent.com/1445834/202573057-5d50e573-1453-4b63-8e1e-6505fa0261c6.png">
<img width="605" alt="Screen Shot 2022-12-27 at 1 18 12 PM"
src="https://user-images.githubusercontent.com/1445834/209712784-34c2384b-bcc8-4db9-a42d-052d81099a40.png">

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Zacqary Adam Xeper 2023-01-10 14:01:44 -06:00 committed by GitHub
parent ff406a55d1
commit ff6024defc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 986 additions and 336 deletions

View file

@ -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;

View file

@ -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 {

View file

@ -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);
});

View file

@ -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;
}

View file

@ -69,11 +69,12 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
: {}),
...(actions
? {
actions: actions.map(({ group, id, actionTypeId, params }) => ({
actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({
group,
id,
params,
connector_type_id: actionTypeId,
frequency,
})),
}
: {}),

View file

@ -59,11 +59,12 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
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 } : {}),

View file

@ -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 () => {

View file

@ -63,10 +63,7 @@ export const rewriteRule = ({
connector_type_id: actionTypeId,
...(frequency
? {
frequency: {
...frequency,
notify_when: frequency.notifyWhen,
},
frequency,
}
: {}),
})),

View file

@ -54,11 +54,12 @@ const rewriteBodyRes: RewriteResponseCase<ResolvedSanitizedRule<RuleTypeParams>>
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 } : {}),

View file

@ -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<UpdateOptions<{ otherField: boolean }>['data']> = {

View file

@ -96,11 +96,12 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
: {}),
...(actions
? {
actions: actions.map(({ group, id, actionTypeId, params }) => ({
actions: actions.map(({ group, id, actionTypeId, params, frequency }) => ({
group,
id,
params,
connector_type_id: actionTypeId,
frequency,
})),
}
: {}),

View file

@ -19,24 +19,8 @@ export async function validateActions(
data: Pick<RawRule, 'notifyWhen' | 'throttle'> & { actions: NormalizedAlertAction[] }
): Promise<void> {
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(', '),
},

View file

@ -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<RawRule>['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;
}

View file

@ -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<Params extends RuleTypeParams = never>(
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)

View file

@ -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<Params extends RuleTypeParams>(
): Promise<PartialRule<Params>> {
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);

View file

@ -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();

View file

@ -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,
},
},
],
},

View file

@ -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"
/>

View file

@ -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,
},
});
});

View file

@ -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<RuleTypeParams>({
data: {
enabled: true,
@ -155,8 +162,6 @@ export class BaseRule {
consumer: 'monitoring',
name,
alertTypeId,
throttle,
notifyWhen: null,
schedule: { interval },
actions: ruleActions,
},

View file

@ -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 = () => {

View file

@ -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<Props> = ({ 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<Props> = ({ field, messageVariables }) =
setActionIdByIndex,
setActions: setAlertActionsProperty,
setActionParamsProperty,
setActionFrequencyProperty: () => {},
featureId: SecurityConnectorFeatureId,
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
hideActionHeader: true,
hideNotifyWhen: true,
}),
[
actions,

View file

@ -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

View file

@ -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é",

View file

@ -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": "セキュリティ検出ルール",

View file

@ -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": "安全检测规则",

View file

@ -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",

View file

@ -13,11 +13,13 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
id,
connector_type_id: actionTypeId,
params,
frequency,
}) => ({
group,
id,
params,
actionTypeId,
frequency,
});
const transformExecutionStatus: RewriteRequestCase<RuleExecutionStatus> = ({

View file

@ -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',

View file

@ -22,17 +22,20 @@ type RuleCreateBody = Omit<
>;
const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
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,
},
})),
});

View file

@ -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\\":[]}",
},
]
`);

View file

@ -15,17 +15,17 @@ type RuleUpdatesBody = Pick<
RuleUpdates,
'name' | 'tags' | 'schedule' | 'actions' | 'params' | 'throttle' | 'notifyWhen'
>;
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({
notifyWhen,
actions,
...res
}): any => ({
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ 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<RuleUpdates, 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
id: string;
}): Promise<Rule> {
const res = await http.put<AsApiContract<Rule>>(
`${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']))
),
}
);

View file

@ -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}
/>

View file

@ -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}
/>
);
})}

View file

@ -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<EuiSuperSelectOption<RuleNotifyWhenType>> = [
{
value: 'onActionGroupChange',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display',
{
defaultMessage: 'On status changes',
}
),
'data-test-subj': 'onActionGroupChange',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On status changes"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if the alert status changes."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description"
/>
</p>
</EuiText>
</>
),
},
{
value: 'onActiveAlert',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display',
{
defaultMessage: 'On check intervals',
}
),
'data-test-subj': 'onActiveAlert',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On check intervals"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if rule conditions are met."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description"
/>
</p>
</EuiText>
</>
),
},
{
value: 'onThrottleInterval',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display',
{
defaultMessage: 'On custom action intervals',
}
),
'data-test-subj': 'onThrottleInterval',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On custom action intervals"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if rule conditions are met."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description"
/>
</p>
</EuiText>
</>
),
},
];
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<boolean>(false);
const [notifyWhenValue, setNotifyWhenValue] =
useState<RuleNotifyWhenType>(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',
}),
<EuiIconTip
position="right"
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip', {
defaultMessage: 'Define how often alerts generate actions.',
})}
/>,
];
return (
<>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiSuperSelect
fullWidth
prepend={labelForRuleRenotify}
data-test-subj="notifyWhenSelect"
options={NOTIFY_WHEN_OPTIONS}
valueOfSelected={notifyWhenValue}
onChange={onNotifyWhenValueChange}
/>
{showCustomThrottleOpts && (
<>
<EuiSpacer size="xs" />
<EuiFormRow fullWidth>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={2}>
<EuiFieldNumber
min={1}
value={throttle ?? 1}
name="throttle"
data-test-subj="throttleInput"
prepend={i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label',
{
defaultMessage: 'Every',
}
)}
onChange={(e) => {
pipe(
some(e.target.value.trim()),
filter((value) => value !== ''),
map((value) => parseInt(value, 10)),
filter((value) => !isNaN(value)),
map((value) => {
onThrottleChange(value, throttleUnit);
})
);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSelect
data-test-subj="throttleUnitInput"
value={throttleUnit}
options={getTimeOptions(throttle ?? 1)}
onChange={(e) => {
onThrottleChange(throttle, e.target.value);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -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}

View file

@ -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<number | null>(
actionItem.frequency?.throttle
? getDurationNumberInItsUnit(actionItem.frequency.throttle)
: null
);
const [actionThrottleUnit, setActionThrottleUnit] = useState<string>(
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 = (
<ActionNotifyWhen
frequency={actionItem.frequency}
throttle={actionThrottle}
throttleUnit={actionThrottleUnit}
onNotifyWhenChange={useCallback(
(notifyWhen) => {
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 && (
<>
<EuiSpacer size="xs" />
<EuiSuperSelect
prepend={
<EuiFormLabel htmlFor={`addNewActionConnectorActionGroup-${actionItem.actionTypeId}`}>
@ -226,10 +279,10 @@ export const ActionTypeForm = ({
setActionGroup(group);
}}
/>
<EuiSpacer size="l" />
</>
)}
{!hideNotifyWhen && actionNotifyWhen}
{(showSelectActionGroup || !hideNotifyWhen) && <EuiSpacer size="l" />}
<EuiFormRow
fullWidth
label={

View file

@ -74,7 +74,9 @@ describe('Rule Actions', () => {
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);

View file

@ -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 <EuiLoadingSpinner size="s" />;
return (
<EuiFlexGroup direction="column" gutterSize="none">
{ruleActions.map(({ actionTypeId, id }, index) => {
{ruleActions.map((action, index) => {
const { actionTypeId, id } = action;
const actionName = getActionName(id);
return (
<EuiFlexItem key={index}>
@ -73,8 +88,23 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp
>
{actionName}
</EuiText>
<EuiFlexGroup alignItems="center" gutterSize="xs" component="span">
<EuiSpacer size="xs" />
<EuiFlexItem grow={false}>
<EuiIcon size="s" type="bell" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText
data-test-subj={`actionConnectorName-${index}-${actionName || actionTypeId}`}
size="xs"
>
{String(getNotifyText(action))}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</EuiFlexItem>
);

View file

@ -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<RuleDefinitionProps> = ({
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<RuleDefinitionProps> = ({
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup>
<ItemTitleRuleSummary>
{i18n.translate('xpack.triggersActionsUI.ruleDetails.notifyWhen', {
defaultMessage: 'Notify',
})}
</ItemTitleRuleSummary>
<ItemValueRuleSummary itemValue={String(getNotifyText())} />
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="baseline">
@ -223,7 +209,11 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
})}
</ItemTitleRuleSummary>
<EuiFlexItem grow={3}>
<RuleActions ruleActions={rule.actions} actionTypeRegistry={actionTypeRegistry} />
<RuleActions
ruleActions={rule.actions}
actionTypeRegistry={actionTypeRegistry}
legacyNotifyWhen={rule.notifyWhen}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -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 } : {}),
};
}

View file

@ -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');

View file

@ -65,7 +65,6 @@ const RuleAdd = ({
},
actions: [],
tags: [],
notifyWhen: 'onActionGroupChange',
...(initialValues ? initialValues : {}),
};
}, [ruleTypeId, consumer, initialValues]);

View file

@ -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<boolean>(false);
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);

View file

@ -233,7 +233,12 @@ describe('rule_form', () => {
describe('rule_form create rule', () => {
let wrapper: ReactWrapper<any>;
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(

View file

@ -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<number | null>(
rule.throttle ? getDurationNumberInItsUnit(rule.throttle) : null
);
const [ruleThrottleUnit, setRuleThrottleUnit] = useState<string>(
rule.throttle ? getDurationUnitValue(rule.throttle) : 'h'
);
const [defaultActionGroupId, setDefaultActionGroupId] = useState<string | undefined>(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 = ({
</Fragment>
));
const labelForRuleChecked = [
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel', {
defaultMessage: 'Check every',
}),
<EuiIconTip
position="right"
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip', {
defaultMessage:
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.',
})}
/>,
];
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 = (
<>
<EuiHorizontalRule />
@ -513,7 +565,7 @@ export const RuleForm = ({
<RuleParamsExpressionComponent
ruleParams={rule.params}
ruleInterval={`${ruleInterval ?? 1}${ruleIntervalUnit}`}
ruleThrottle={`${ruleThrottle ?? 1}${ruleThrottleUnit}`}
ruleThrottle={''}
alertNotifyWhen={rule.notifyWhen ?? 'onActionGroupChange'}
errors={errors}
setRuleParams={setRuleParams}
@ -530,6 +582,51 @@ export const RuleForm = ({
</Suspense>
</EuiErrorBoundary>
) : null}
<EuiFlexItem>
<EuiFormRow
fullWidth
data-test-subj="intervalFormRow"
display="rowCompressed"
helpText={getHelpTextForInterval()}
isInvalid={errors['schedule.interval'].length > 0}
error={errors['schedule.interval']}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={2}>
<EuiFieldNumber
prepend={labelForRuleChecked}
fullWidth
min={1}
isInvalid={errors['schedule.interval'].length > 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}`);
}
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSelect
fullWidth
value={ruleIntervalUnit}
options={getTimeOptions(ruleInterval ?? 1)}
onChange={(e) => {
setRuleIntervalUnit(e.target.value);
setScheduleProperty('interval', `${ruleInterval}${e.target.value}`);
}}
data-test-subj="intervalInputUnit"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFlexItem>
<EuiSpacer size="l" />
{canShowActions &&
defaultActionGroupId &&
ruleTypeModel &&
@ -543,6 +640,7 @@ export const RuleForm = ({
<EuiSpacer />
</>
) : null}
<EuiSpacer size="m" />
<ActionForm
actions={rule.actions}
setHasActionsDisabled={setHasActionsDisabled}
@ -573,70 +671,16 @@ export const RuleForm = ({
setActions={setActions}
setActionParamsProperty={setActionParamsProperty}
actionTypeRegistry={actionTypeRegistry}
setActionFrequencyProperty={setActionFrequencyProperty}
/>
</>
) : null}
</>
);
const labelForRuleChecked = (
<>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel"
defaultMessage="Check every"
/>{' '}
<EuiIconTip
position="right"
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip', {
defaultMessage:
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.',
})}
/>
</>
);
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 (
<EuiForm>
<EuiFlexGrid columns={2}>
<EuiFlexGrid columns={1}>
<EuiFlexItem>
<EuiFormRow
fullWidth
@ -703,74 +747,6 @@ export const RuleForm = ({
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFormRow
fullWidth
data-test-subj="intervalFormRow"
display="rowCompressed"
helpText={getHelpTextForInterval()}
label={labelForRuleChecked}
isInvalid={errors['schedule.interval'].length > 0}
error={errors['schedule.interval']}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldNumber
fullWidth
min={1}
isInvalid={errors['schedule.interval'].length > 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}`);
}
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSelect
fullWidth
value={ruleIntervalUnit}
options={getTimeOptions(ruleInterval ?? 1)}
onChange={(e) => {
setRuleIntervalUnit(e.target.value);
setScheduleProperty('interval', `${ruleInterval}${e.target.value}`);
}}
data-test-subj="intervalInputUnit"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<RuleNotifyWhen
rule={rule}
throttle={ruleThrottle}
throttleUnit={ruleThrottleUnit}
onNotifyWhenChange={useCallback(
(notifyWhen) => {
setRuleProperty('notifyWhen', notifyWhen);
},
[setRuleProperty]
)}
onThrottleChange={useCallback(
(throttle: number | null, throttleUnit: string) => {
setRuleThrottle(throttle);
setRuleThrottleUnit(throttleUnit);
setRuleProperty('throttle', throttle ? `${throttle}${throttleUnit}` : null);
},
[setRuleProperty]
)}
/>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="m" />
{ruleTypeModel ? (
<>{ruleTypeDetails}</>
) : availableRuleTypes.length ? (

View file

@ -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');
});
});

View file

@ -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<Rule> &
Pick<Rule, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags' | 'notifyWhen'>;
Pick<Rule, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>;
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<keyof RuleAction>;
payload: Payload<string, RuleActionParam>;
}
| {
command: CommandType<'setRuleActionFrequency'>;
payload: Payload<string, RuleActionParam>;
};
export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>;
@ -179,6 +185,36 @@ export const ruleReducer = <RulePhase extends InitialRule | Rule>(
};
}
}
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<keyof RuleAction>;
if (index === undefined || isEqual(rule.actions[index][key], value)) {

View file

@ -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,
};

View file

@ -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';

View file

@ -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 () => {

View file

@ -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;

View file

@ -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');

View file

@ -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,
},
},
],
},

View file

@ -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) => {

View file

@ -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,
}
);

View file

@ -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();

View file

@ -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,