mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
ff406a55d1
commit
ff6024defc
58 changed files with 986 additions and 336 deletions
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
|
|
|
@ -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 } : {}),
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -63,10 +63,7 @@ export const rewriteRule = ({
|
|||
connector_type_id: actionTypeId,
|
||||
...(frequency
|
||||
? {
|
||||
frequency: {
|
||||
...frequency,
|
||||
notify_when: frequency.notifyWhen,
|
||||
},
|
||||
frequency,
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
|
|
|
@ -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 } : {}),
|
||||
|
|
|
@ -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']> = {
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
|
|
|
@ -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(', '),
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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é",
|
||||
|
|
|
@ -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": "セキュリティ検出ルール",
|
||||
|
|
|
@ -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": "安全检测规则",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -13,11 +13,13 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
|
|||
id,
|
||||
connector_type_id: actionTypeId,
|
||||
params,
|
||||
frequency,
|
||||
}) => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
actionTypeId,
|
||||
frequency,
|
||||
});
|
||||
|
||||
const transformExecutionStatus: RewriteRequestCase<RuleExecutionStatus> = ({
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
|
|
|
@ -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\\":[]}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -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']))
|
||||
),
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -65,7 +65,6 @@ const RuleAdd = ({
|
|||
},
|
||||
actions: [],
|
||||
tags: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
...(initialValues ? initialValues : {}),
|
||||
};
|
||||
}, [ruleTypeId, consumer, initialValues]);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue