[Actions] Notify only on action group change (#82969)
* plugged Task Manager lifecycle into status reactively
* fixed tests
* Revert "fixed tests"
This reverts commit e9f2cd05bd
.
* made action group fields optional
* revert deletion
* again
* extracted action type for mto its own component
* extracted more sections of the action form to their own components
* updated icon
* added docs
* fixed always firing alert
* fixed export of components
* fixed react warning
* Adding flag for notifying on state change
* Updating logic in task runner
* Starting to update tests
* Adding tests
* Fixing types check
* Tests and types
* Tests
* Tests
* Tests
* Tests
* Tests
* Renaming field to a more descriptive name. Adding migrations
* Renaming field to a more descriptive name. Adding migrations
* Fixing tests
* Type check and tests
* Moving schedule and notify interval to bottom of flyout. Implementing dropdown from mockup in new component
* Changing boolean flag to enum type and updating in triggers_actions_ui
* Changing boolean flag to enum type and updating in alerts plugin
* Fixing types check
* Fixing monitoring jest tests
* Changing last references to old variable names
* Moving form inputs back to the top
* Renaming to alert_notify_when
* Updating functional tests
* Adding new functional test for notifyWhen onActionGroupChange
* Updating wording
* Incorporating action subgroups into logic
* PR fixes
* Updating functional test
* Fixing types check
* Changing default throttle interval to hour
* Fixing types check
Co-authored-by: Gidi Meir Morris <github@gidi.io>
This commit is contained in:
parent
317608420a
commit
ab082647ac
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
|
||||
import { AlertNotifyWhenType } from './alert_notify_when_type';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AlertTypeState = Record<string, any>;
|
||||
|
@ -68,6 +69,7 @@ export interface Alert {
|
|||
apiKey: string | null;
|
||||
apiKeyOwner: string | null;
|
||||
throttle: string | null;
|
||||
notifyWhen: AlertNotifyWhenType | null;
|
||||
muteAll: boolean;
|
||||
mutedInstanceIds: string[];
|
||||
executionStatus: AlertExecutionStatus;
|
||||
|
|
18
x-pack/plugins/alerts/common/alert_notify_when_type.test.ts
Normal file
18
x-pack/plugins/alerts/common/alert_notify_when_type.test.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { validateNotifyWhenType } from './alert_notify_when_type';
|
||||
|
||||
test('validates valid notify when type', () => {
|
||||
expect(validateNotifyWhenType('onActionGroupChange')).toBeUndefined();
|
||||
expect(validateNotifyWhenType('onActiveAlert')).toBeUndefined();
|
||||
expect(validateNotifyWhenType('onThrottleInterval')).toBeUndefined();
|
||||
});
|
||||
test('returns error string if input is not valid notify when type', () => {
|
||||
expect(validateNotifyWhenType('randomString')).toEqual(
|
||||
`string is not a valid AlertNotifyWhenType: randomString`
|
||||
);
|
||||
});
|
19
x-pack/plugins/alerts/common/alert_notify_when_type.ts
Normal file
19
x-pack/plugins/alerts/common/alert_notify_when_type.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
const AlertNotifyWhenTypeValues = [
|
||||
'onActionGroupChange',
|
||||
'onActiveAlert',
|
||||
'onThrottleInterval',
|
||||
] as const;
|
||||
export type AlertNotifyWhenType = typeof AlertNotifyWhenTypeValues[number];
|
||||
|
||||
export function validateNotifyWhenType(notifyWhen: string) {
|
||||
if (AlertNotifyWhenTypeValues.includes(notifyWhen as AlertNotifyWhenType)) {
|
||||
return;
|
||||
}
|
||||
return `string is not a valid AlertNotifyWhenType: ${notifyWhen}`;
|
||||
}
|
|
@ -14,6 +14,7 @@ export * from './alert_navigation';
|
|||
export * from './alert_instance_summary';
|
||||
export * from './builtin_action_groups';
|
||||
export * from './disabled_action_groups';
|
||||
export * from './alert_notify_when_type';
|
||||
|
||||
export interface AlertingFrameworkHealth {
|
||||
isSufficientlySecure: boolean;
|
||||
|
|
|
@ -72,6 +72,114 @@ describe('isThrottled', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('scheduledActionGroupOrSubgroupHasChanged()', () => {
|
||||
test('should be false if no last scheduled and nothing scheduled', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should be false if group does not change', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActions('default');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should be false if group and subgroup does not change', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should be false if group does not change and subgroup goes from undefined to defined', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'subgroup');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should be false if group does not change and subgroup goes from defined to undefined', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActions('default');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should be true if no last scheduled and has scheduled action', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
alertInstance.scheduleActions('default');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should be true if group does change', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActions('penguin');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should be true if group does change and subgroup does change', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActionsWithSubGroup('penguin', 'fish');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true);
|
||||
});
|
||||
|
||||
test('should be true if group does not change and subgroup does change', () => {
|
||||
const alertInstance = new AlertInstance({
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: new Date(),
|
||||
group: 'default',
|
||||
subgroup: 'subgroup',
|
||||
},
|
||||
},
|
||||
});
|
||||
alertInstance.scheduleActionsWithSubGroup('default', 'fish');
|
||||
expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getScheduledActionOptions()', () => {
|
||||
test('defaults to undefined', () => {
|
||||
const alertInstance = new AlertInstance();
|
||||
|
|
|
@ -70,6 +70,31 @@ export class AlertInstance<
|
|||
return false;
|
||||
}
|
||||
|
||||
scheduledActionGroupOrSubgroupHasChanged(): boolean {
|
||||
if (!this.meta.lastScheduledActions && this.scheduledExecutionOptions) {
|
||||
// it is considered a change when there are no previous scheduled actions
|
||||
// and new scheduled actions
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.meta.lastScheduledActions && this.scheduledExecutionOptions) {
|
||||
// compare previous and new scheduled actions if both exist
|
||||
return (
|
||||
!this.scheduledActionGroupIsUnchanged(
|
||||
this.meta.lastScheduledActions,
|
||||
this.scheduledExecutionOptions
|
||||
) ||
|
||||
!this.scheduledActionSubgroupIsUnchanged(
|
||||
this.meta.lastScheduledActions,
|
||||
this.scheduledExecutionOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// no previous and no new scheduled actions
|
||||
return false;
|
||||
}
|
||||
|
||||
private scheduledActionGroupIsUnchanged(
|
||||
lastScheduledActions: NonNullable<AlertInstanceMeta['lastScheduledActions']>,
|
||||
scheduledExecutionOptions: ScheduledExecutionOptions<State, Context>
|
||||
|
|
|
@ -29,8 +29,13 @@ import {
|
|||
AlertTaskState,
|
||||
AlertInstanceSummary,
|
||||
AlertExecutionStatusValues,
|
||||
AlertNotifyWhenType,
|
||||
} from '../types';
|
||||
import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib';
|
||||
import {
|
||||
validateAlertTypeParams,
|
||||
alertExecutionStatusFromRaw,
|
||||
getAlertNotifyWhenType,
|
||||
} from '../lib';
|
||||
import {
|
||||
GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult,
|
||||
InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
|
||||
|
@ -157,6 +162,7 @@ interface UpdateOptions {
|
|||
actions: NormalizedAlertAction[];
|
||||
params: Record<string, unknown>;
|
||||
throttle: string | null;
|
||||
notifyWhen: AlertNotifyWhenType | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -251,6 +257,8 @@ export class AlertsClient {
|
|||
const createTime = Date.now();
|
||||
const { references, actions } = await this.denormalizeActions(data.actions);
|
||||
|
||||
const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle);
|
||||
|
||||
const rawAlert: RawAlert = {
|
||||
...data,
|
||||
...this.apiKeyAsAlertAttributes(createdAPIKey, username),
|
||||
|
@ -262,6 +270,7 @@ export class AlertsClient {
|
|||
params: validatedAlertTypeParams as RawAlert['params'],
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen,
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastExecutionDate: new Date().toISOString(),
|
||||
|
@ -694,6 +703,7 @@ export class AlertsClient {
|
|||
? await this.createAPIKey(this.generateAPIKeyName(alertType.id, data.name))
|
||||
: null;
|
||||
const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username);
|
||||
const notifyWhen = getAlertNotifyWhenType(data.notifyWhen, data.throttle);
|
||||
|
||||
let updatedObject: SavedObject<RawAlert>;
|
||||
const createAttributes = this.updateMeta({
|
||||
|
@ -702,6 +712,7 @@ export class AlertsClient {
|
|||
...apiKeyAttributes,
|
||||
params: validatedAlertTypeParams as RawAlert['params'],
|
||||
actions,
|
||||
notifyWhen,
|
||||
updatedBy: username,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
@ -1326,7 +1337,7 @@ export class AlertsClient {
|
|||
|
||||
private getPartialAlertFromRaw(
|
||||
id: string,
|
||||
{ createdAt, updatedAt, meta, scheduledTaskId, ...rawAlert }: Partial<RawAlert>,
|
||||
{ createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial<RawAlert>,
|
||||
references: SavedObjectReference[] | undefined
|
||||
): PartialAlert {
|
||||
// Not the prettiest code here, but if we want to use most of the
|
||||
|
@ -1341,6 +1352,7 @@ export class AlertsClient {
|
|||
const executionStatus = alertExecutionStatusFromRaw(this.logger, id, rawAlert.executionStatus);
|
||||
return {
|
||||
id,
|
||||
notifyWhen,
|
||||
...rawAlertWithoutExecutionStatus,
|
||||
// we currently only support the Interval Schedule type
|
||||
// Once we support additional types, this type signature will likely change
|
||||
|
|
|
@ -68,6 +68,7 @@ function getMockData(overwrites: Record<string, unknown> = {}): CreateOptions['d
|
|||
consumer: 'bar',
|
||||
schedule: { interval: '10s' },
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
|
@ -341,6 +342,7 @@ describe('create()', () => {
|
|||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "abc",
|
||||
"notifyWhen": null,
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -389,6 +391,7 @@ describe('create()', () => {
|
|||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -488,6 +491,7 @@ describe('create()', () => {
|
|||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -587,6 +591,7 @@ describe('create()', () => {
|
|||
"alertTypeId": "123",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -626,6 +631,7 @@ describe('create()', () => {
|
|||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -662,6 +668,7 @@ describe('create()', () => {
|
|||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"enabled": false,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -740,6 +747,426 @@ describe('create()', () => {
|
|||
expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name');
|
||||
});
|
||||
|
||||
test('should create alert with given notifyWhen value if notifyWhen is not null', async () => {
|
||||
const data = getMockData({ notifyWhen: 'onActionGroupChange', throttle: '10m' });
|
||||
const createdAttributes = {
|
||||
...data,
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: createdAttributes,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
taskType: 'alerting:123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 1,
|
||||
status: TaskStatus.Idle,
|
||||
runAt: new Date(),
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
state: {},
|
||||
params: {},
|
||||
ownerId: null,
|
||||
});
|
||||
const result = await alertsClient.create({ data });
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
actionTypeId: 'test',
|
||||
params: { foo: true },
|
||||
},
|
||||
],
|
||||
alertTypeId: '123',
|
||||
consumer: 'bar',
|
||||
name: 'abc',
|
||||
params: { bar: true },
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
createdBy: 'elastic',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
enabled: true,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
schedule: { interval: '10s' },
|
||||
throttle: '10m',
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
tags: ['foo'],
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mock-saved-object-id',
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"consumer": "bar",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"createdBy": "elastic",
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActionGroupChange",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"scheduledTaskId": "task-123",
|
||||
"tags": Array [
|
||||
"foo",
|
||||
],
|
||||
"throttle": "10m",
|
||||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should create alert with notifyWhen = onThrottleInterval if notifyWhen is null and throttle is set', async () => {
|
||||
const data = getMockData({ throttle: '10m' });
|
||||
const createdAttributes = {
|
||||
...data,
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: createdAttributes,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
taskType: 'alerting:123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 1,
|
||||
status: TaskStatus.Idle,
|
||||
runAt: new Date(),
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
state: {},
|
||||
params: {},
|
||||
ownerId: null,
|
||||
});
|
||||
const result = await alertsClient.create({ data });
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
actionTypeId: 'test',
|
||||
params: { foo: true },
|
||||
},
|
||||
],
|
||||
alertTypeId: '123',
|
||||
consumer: 'bar',
|
||||
name: 'abc',
|
||||
params: { bar: true },
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
createdBy: 'elastic',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
enabled: true,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
schedule: { interval: '10s' },
|
||||
throttle: '10m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
tags: ['foo'],
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mock-saved-object-id',
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"consumer": "bar",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"createdBy": "elastic",
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "abc",
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"scheduledTaskId": "task-123",
|
||||
"tags": Array [
|
||||
"foo",
|
||||
],
|
||||
"throttle": "10m",
|
||||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should create alert with notifyWhen = onActiveAlert if notifyWhen is null and throttle is null', async () => {
|
||||
const data = getMockData();
|
||||
const createdAttributes = {
|
||||
...data,
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: createdAttributes,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
taskType: 'alerting:123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 1,
|
||||
status: TaskStatus.Idle,
|
||||
runAt: new Date(),
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
state: {},
|
||||
params: {},
|
||||
ownerId: null,
|
||||
});
|
||||
const result = await alertsClient.create({ data });
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
actionTypeId: 'test',
|
||||
params: { foo: true },
|
||||
},
|
||||
],
|
||||
alertTypeId: '123',
|
||||
consumer: 'bar',
|
||||
name: 'abc',
|
||||
params: { bar: true },
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
createdBy: 'elastic',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
enabled: true,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
schedule: { interval: '10s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
tags: ['foo'],
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mock-saved-object-id',
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionTypeId": "test",
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"consumer": "bar",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"createdBy": "elastic",
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"scheduledTaskId": "task-123",
|
||||
"tags": Array [
|
||||
"foo",
|
||||
],
|
||||
"throttle": null,
|
||||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should validate params', async () => {
|
||||
const data = getMockData();
|
||||
alertTypeRegistry.get.mockReturnValue({
|
||||
|
@ -1049,6 +1476,7 @@ describe('create()', () => {
|
|||
},
|
||||
schedule: { interval: '10s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
tags: ['foo'],
|
||||
|
@ -1172,6 +1600,7 @@ describe('create()', () => {
|
|||
},
|
||||
schedule: { interval: '10s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
tags: ['foo'],
|
||||
|
|
|
@ -85,6 +85,7 @@ describe('find()', () => {
|
|||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -143,6 +144,7 @@ describe('find()', () => {
|
|||
"alertTypeId": "myType",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -234,6 +236,7 @@ describe('find()', () => {
|
|||
Object {
|
||||
"actions": Array [],
|
||||
"id": "1",
|
||||
"notifyWhen": undefined,
|
||||
"schedule": undefined,
|
||||
"tags": Array [
|
||||
"myTag",
|
||||
|
|
|
@ -72,6 +72,7 @@ describe('get()', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
|
@ -96,6 +97,7 @@ describe('get()', () => {
|
|||
"alertTypeId": "123",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
|
|
@ -80,6 +80,7 @@ const BaseAlertInstanceSummarySavedObject: SavedObject<RawAlert> = {
|
|||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -70,6 +70,7 @@ describe('update()', () => {
|
|||
scheduledTaskId: 'task-123',
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -144,6 +145,7 @@ describe('update()', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
scheduledTaskId: 'task-123',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
@ -185,6 +187,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -241,6 +244,7 @@ describe('update()', () => {
|
|||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -295,6 +299,7 @@ describe('update()', () => {
|
|||
"versionApiKeyLastmodified": "v7.10.0",
|
||||
},
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -368,6 +373,7 @@ describe('update()', () => {
|
|||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -418,6 +424,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: '5m',
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -445,6 +452,7 @@ describe('update()', () => {
|
|||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -479,6 +487,7 @@ describe('update()', () => {
|
|||
"versionApiKeyLastmodified": "v7.10.0",
|
||||
},
|
||||
"name": "abc",
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -540,6 +549,7 @@ describe('update()', () => {
|
|||
params: {
|
||||
bar: true,
|
||||
},
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
|
@ -583,6 +593,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: '5m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -611,6 +622,7 @@ describe('update()', () => {
|
|||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"enabled": false,
|
||||
"id": "1",
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -645,6 +657,7 @@ describe('update()', () => {
|
|||
"versionApiKeyLastmodified": "v7.10.0",
|
||||
},
|
||||
"name": "abc",
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
@ -702,6 +715,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -830,6 +844,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -937,6 +952,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: '5m',
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -998,6 +1014,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -1118,6 +1135,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -1149,6 +1167,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -1185,6 +1204,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -1220,6 +1240,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -1273,6 +1294,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
@ -1296,6 +1318,7 @@ describe('update()', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [],
|
||||
},
|
||||
})
|
||||
|
@ -1339,6 +1362,7 @@ describe('update()', () => {
|
|||
},
|
||||
throttle: null,
|
||||
actions: [],
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1368,6 +1392,7 @@ describe('update()', () => {
|
|||
},
|
||||
throttle: null,
|
||||
actions: [],
|
||||
notifyWhen: null,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
|
|
@ -105,6 +105,7 @@ async function update(success: boolean) {
|
|||
tags: ['bar'],
|
||||
params: { bar: true },
|
||||
throttle: '10s',
|
||||
notifyWhen: null,
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -648,6 +648,7 @@ const BaseAlert: SanitizedAlert = {
|
|||
tags: [],
|
||||
consumer: 'alert-consumer',
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
params: { bar: true },
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getAlertNotifyWhenType } from './get_alert_notify_when_type';
|
||||
|
||||
test(`should return 'notifyWhen' value if value is set and throttle is null`, () => {
|
||||
expect(getAlertNotifyWhenType('onActionGroupChange', null)).toEqual('onActionGroupChange');
|
||||
});
|
||||
|
||||
test(`should return 'notifyWhen' value if value is set and throttle is defined`, () => {
|
||||
expect(getAlertNotifyWhenType('onActionGroupChange', '10m')).toEqual('onActionGroupChange');
|
||||
});
|
||||
|
||||
test(`should return 'onThrottleInterval' value if 'notifyWhen' is null and throttle is defined`, () => {
|
||||
expect(getAlertNotifyWhenType(null, '10m')).toEqual('onThrottleInterval');
|
||||
});
|
||||
|
||||
test(`should return 'onActiveAlert' value if 'notifyWhen' is null and throttle is null`, () => {
|
||||
expect(getAlertNotifyWhenType(null, null)).toEqual('onActiveAlert');
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertNotifyWhenType } from '../types';
|
||||
|
||||
export function getAlertNotifyWhenType(
|
||||
notifyWhen: AlertNotifyWhenType | null,
|
||||
throttle: string | null
|
||||
): AlertNotifyWhenType {
|
||||
// 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';
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
export { parseDuration, validateDurationSchema } from '../../common/parse_duration';
|
||||
export { LicenseState } from './license_state';
|
||||
export { validateAlertTypeParams } from './validate_alert_type_params';
|
||||
export { getAlertNotifyWhenType } from './get_alert_notify_when_type';
|
||||
export { ErrorWithReason, getReasonFromError, isErrorWithReason } from './error_with_reason';
|
||||
export {
|
||||
executionStatusFromState,
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('createAlertRoute', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: '30s',
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -56,6 +57,7 @@ describe('createAlertRoute', () => {
|
|||
apiKey: '',
|
||||
apiKeyOwner: '',
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
createdAt,
|
||||
updatedAt,
|
||||
id: '123',
|
||||
|
@ -110,6 +112,7 @@ describe('createAlertRoute', () => {
|
|||
"alertTypeId": "1",
|
||||
"consumer": "bar",
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActionGroupChange",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import { LicenseState } from '../lib/license_state';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { validateDurationSchema } from '../lib';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
import { Alert, BASE_ALERT_API_PATH } from '../types';
|
||||
import { Alert, AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../types';
|
||||
|
||||
export const bodySchema = schema.object({
|
||||
name: schema.string(),
|
||||
|
@ -38,6 +38,7 @@ export const bodySchema = schema.object({
|
|||
}),
|
||||
{ defaultValue: [] }
|
||||
),
|
||||
notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })),
|
||||
});
|
||||
|
||||
export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => {
|
||||
|
@ -61,7 +62,8 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const alert = req.body;
|
||||
const alertRes: Alert = await alertsClient.create({ data: alert });
|
||||
const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as AlertNotifyWhenType) : null;
|
||||
const alertRes: Alert = await alertsClient.create({ data: { ...alert, notifyWhen } });
|
||||
return res.ok({
|
||||
body: alertRes,
|
||||
});
|
||||
|
|
|
@ -46,6 +46,7 @@ describe('getAlertRoute', () => {
|
|||
tags: ['foo'],
|
||||
enabled: true,
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
createdBy: '',
|
||||
updatedBy: '',
|
||||
apiKey: '',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { mockLicenseState } from '../lib/license_state.mock';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { alertsClientMock } from '../alerts_client.mock';
|
||||
import { AlertNotifyWhenType } from '../../common';
|
||||
|
||||
const alertsClient = alertsClientMock.create();
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
|
@ -41,6 +42,7 @@ describe('updateAlertRoute', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType,
|
||||
};
|
||||
|
||||
it('updates an alert with proper parameters', async () => {
|
||||
|
@ -78,6 +80,7 @@ describe('updateAlertRoute', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
|
@ -100,6 +103,7 @@ describe('updateAlertRoute', () => {
|
|||
},
|
||||
],
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActionGroupChange",
|
||||
"params": Object {
|
||||
"otherField": false,
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import { LicenseState } from '../lib/license_state';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { validateDurationSchema } from '../lib';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
import { BASE_ALERT_API_PATH } from '../../common';
|
||||
import { AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
|
@ -39,6 +39,7 @@ const bodySchema = schema.object({
|
|||
}),
|
||||
{ defaultValue: [] }
|
||||
),
|
||||
notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })),
|
||||
});
|
||||
|
||||
export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => {
|
||||
|
@ -62,11 +63,19 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
const { name, actions, params, schedule, tags, throttle } = req.body;
|
||||
const { name, actions, params, schedule, tags, throttle, notifyWhen } = req.body;
|
||||
return res.ok({
|
||||
body: await alertsClient.update({
|
||||
id,
|
||||
data: { name, actions, params, schedule, tags, throttle },
|
||||
data: {
|
||||
name,
|
||||
actions,
|
||||
params,
|
||||
schedule,
|
||||
tags,
|
||||
throttle,
|
||||
notifyWhen: notifyWhen as AlertNotifyWhenType,
|
||||
},
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
|
|
@ -74,6 +74,9 @@
|
|||
"throttle": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notifyWhen": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"muteAll": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -277,6 +277,7 @@ describe('7.11.0', () => {
|
|||
attributes: {
|
||||
...alert.attributes,
|
||||
updatedAt: alert.updated_at,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -289,6 +290,33 @@ describe('7.11.0', () => {
|
|||
attributes: {
|
||||
...alert.attributes,
|
||||
updatedAt: alert.attributes.createdAt,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('add notifyWhen=onActiveAlert when throttle is null', () => {
|
||||
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
|
||||
const alert = getMockData({});
|
||||
expect(migration711(alert, { log })).toEqual({
|
||||
...alert,
|
||||
attributes: {
|
||||
...alert.attributes,
|
||||
updatedAt: alert.attributes.createdAt,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('add notifyWhen=onActiveAlert when throttle is set', () => {
|
||||
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
|
||||
const alert = getMockData({ throttle: '5m' });
|
||||
expect(migration711(alert, { log })).toEqual({
|
||||
...alert,
|
||||
attributes: {
|
||||
...alert.attributes,
|
||||
updatedAt: alert.attributes.createdAt,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,15 +37,18 @@ export function getMigrations(
|
|||
)
|
||||
);
|
||||
|
||||
const migrationAlertUpdatedAtDate = encryptedSavedObjects.createMigration<RawAlert, RawAlert>(
|
||||
// migrate all documents in 7.11 in order to add the "updatedAt" field
|
||||
const migrationAlertUpdatedAtAndNotifyWhen = encryptedSavedObjects.createMigration<
|
||||
RawAlert,
|
||||
RawAlert
|
||||
>(
|
||||
// migrate all documents in 7.11 in order to add the "updatedAt" and "notifyWhen" fields
|
||||
(doc): doc is SavedObjectUnsanitizedDoc<RawAlert> => true,
|
||||
pipeMigrations(setAlertUpdatedAtDate)
|
||||
pipeMigrations(setAlertUpdatedAtDate, setNotifyWhen)
|
||||
);
|
||||
|
||||
return {
|
||||
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
|
||||
'7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtDate, '7.11.0'),
|
||||
'7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,6 +82,19 @@ const setAlertUpdatedAtDate = (
|
|||
};
|
||||
};
|
||||
|
||||
const setNotifyWhen = (
|
||||
doc: SavedObjectUnsanitizedDoc<RawAlert>
|
||||
): SavedObjectUnsanitizedDoc<RawAlert> => {
|
||||
const notifyWhen = doc.attributes.throttle ? 'onThrottleInterval' : 'onActiveAlert';
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
notifyWhen,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const consumersToChange: Map<string, string> = new Map(
|
||||
Object.entries({
|
||||
alerting: 'alerts',
|
||||
|
|
|
@ -27,6 +27,7 @@ const alert: SanitizedAlert = {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -92,6 +92,7 @@ describe('Task Runner', () => {
|
|||
updatedAt: new Date('2019-02-12T21:01:22.479Z'),
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
enabled: true,
|
||||
alertTypeId: alertType.id,
|
||||
apiKey: '',
|
||||
|
@ -533,6 +534,188 @@ describe('Task Runner', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('actionsPlugin.execute is not called when notifyWhen=onActionGroupChange and alert instance state does not change', async () => {
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
alertType.executor.mockImplementation(
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').scheduleActions('default');
|
||||
}
|
||||
);
|
||||
const taskRunner = new TaskRunner(
|
||||
alertType,
|
||||
{
|
||||
...mockedTaskInstance,
|
||||
state: {
|
||||
...mockedTaskInstance.state,
|
||||
alertInstances: {
|
||||
'1': {
|
||||
meta: {
|
||||
lastScheduledActions: { date: '1970-01-01T00:00:00.000Z', group: 'default' },
|
||||
},
|
||||
state: { bar: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
alertsClient.get.mockResolvedValue({
|
||||
...mockedAlertTypeSavedObject,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await taskRunner.run();
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0);
|
||||
|
||||
const eventLogger = taskRunnerFactoryInitializerParams.eventLogger;
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
|
||||
expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "active-instance",
|
||||
},
|
||||
"kibana": Object {
|
||||
"alerting": Object {
|
||||
"action_group_id": "default",
|
||||
"instance_id": "1",
|
||||
},
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": undefined,
|
||||
"rel": "primary",
|
||||
"type": "alert",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"@timestamp": "1970-01-01T00:00:00.000Z",
|
||||
"event": Object {
|
||||
"action": "execute",
|
||||
"outcome": "success",
|
||||
},
|
||||
"kibana": Object {
|
||||
"alerting": Object {
|
||||
"status": "active",
|
||||
},
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"namespace": undefined,
|
||||
"rel": "primary",
|
||||
"type": "alert",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "alert executed: test:1: 'alert-name'",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('actionsPlugin.execute is called when notifyWhen=onActionGroupChange and alert instance state has changed', async () => {
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
alertType.executor.mockImplementation(
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').scheduleActions('default');
|
||||
}
|
||||
);
|
||||
const taskRunner = new TaskRunner(
|
||||
alertType,
|
||||
{
|
||||
...mockedTaskInstance,
|
||||
state: {
|
||||
...mockedTaskInstance.state,
|
||||
alertInstances: {
|
||||
'1': {
|
||||
meta: { lastScheduledActions: { group: 'newGroup', date: new Date().toISOString() } },
|
||||
state: { bar: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
alertsClient.get.mockResolvedValue({
|
||||
...mockedAlertTypeSavedObject,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await taskRunner.run();
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('actionsPlugin.execute is called when notifyWhen=onActionGroupChange and alert instance state subgroup has changed', async () => {
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
alertType.executor.mockImplementation(
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices
|
||||
.alertInstanceFactory('1')
|
||||
.scheduleActionsWithSubGroup('default', 'subgroup1');
|
||||
}
|
||||
);
|
||||
const taskRunner = new TaskRunner(
|
||||
alertType,
|
||||
{
|
||||
...mockedTaskInstance,
|
||||
state: {
|
||||
...mockedTaskInstance.state,
|
||||
alertInstances: {
|
||||
'1': {
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'default',
|
||||
subgroup: 'newSubgroup',
|
||||
date: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
state: { bar: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
taskRunnerFactoryInitializerParams
|
||||
);
|
||||
alertsClient.get.mockResolvedValue({
|
||||
...mockedAlertTypeSavedObject,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
});
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await taskRunner.run();
|
||||
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('includes the apiKey in the request used to initialize the actionsClient', async () => {
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
|
||||
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
|
||||
|
|
|
@ -171,7 +171,16 @@ export class TaskRunner {
|
|||
spaceId: string,
|
||||
event: Event
|
||||
): Promise<AlertTaskState> {
|
||||
const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert;
|
||||
const {
|
||||
throttle,
|
||||
notifyWhen,
|
||||
muteAll,
|
||||
mutedInstanceIds,
|
||||
name,
|
||||
tags,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
} = alert;
|
||||
const {
|
||||
params: { alertId },
|
||||
state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt },
|
||||
|
@ -257,24 +266,39 @@ export class TaskRunner {
|
|||
alertLabel,
|
||||
});
|
||||
|
||||
const instancesToExecute =
|
||||
notifyWhen === 'onActionGroupChange'
|
||||
? Object.entries(instancesWithScheduledActions).filter(
|
||||
([alertInstanceName, alertInstance]: [string, AlertInstance]) => {
|
||||
const shouldExecuteAction = alertInstance.scheduledActionGroupOrSubgroupHasChanged();
|
||||
if (!shouldExecuteAction) {
|
||||
this.logger.debug(
|
||||
`skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is active but action group has not changed`
|
||||
);
|
||||
}
|
||||
return shouldExecuteAction;
|
||||
}
|
||||
)
|
||||
: Object.entries(instancesWithScheduledActions).filter(
|
||||
([alertInstanceName, alertInstance]: [string, AlertInstance]) => {
|
||||
const throttled = alertInstance.isThrottled(throttle);
|
||||
const muted = mutedInstanceIdsSet.has(alertInstanceName);
|
||||
const shouldExecuteAction = !throttled && !muted;
|
||||
if (!shouldExecuteAction) {
|
||||
this.logger.debug(
|
||||
`skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is ${
|
||||
muted ? 'muted' : 'throttled'
|
||||
}`
|
||||
);
|
||||
}
|
||||
return shouldExecuteAction;
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(instancesWithScheduledActions)
|
||||
.filter(([alertInstanceName, alertInstance]: [string, AlertInstance]) => {
|
||||
const throttled = alertInstance.isThrottled(throttle);
|
||||
const muted = mutedInstanceIdsSet.has(alertInstanceName);
|
||||
const shouldExecuteAction = !throttled && !muted;
|
||||
if (!shouldExecuteAction) {
|
||||
this.logger.debug(
|
||||
`skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is ${
|
||||
muted ? 'muted' : 'throttled'
|
||||
}`
|
||||
);
|
||||
}
|
||||
return shouldExecuteAction;
|
||||
})
|
||||
.map(([id, alertInstance]: [string, AlertInstance]) =>
|
||||
this.executeAlertInstance(id, alertInstance, executionHandler)
|
||||
)
|
||||
instancesToExecute.map(([id, alertInstance]: [string, AlertInstance]) =>
|
||||
this.executeAlertInstance(id, alertInstance, executionHandler)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.logger.debug(`no scheduling of actions for alert ${alertLabel}: alert is muted.`);
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
AlertExecutionStatuses,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertsHealth,
|
||||
AlertNotifyWhenType,
|
||||
} from '../common';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
|
@ -152,6 +153,7 @@ export interface RawAlert extends SavedObjectAttributes {
|
|||
apiKey: string | null;
|
||||
apiKeyOwner: string | null;
|
||||
throttle: string | null;
|
||||
notifyWhen: AlertNotifyWhenType | null;
|
||||
muteAll: boolean;
|
||||
mutedInstanceIds: string[];
|
||||
meta?: AlertMeta;
|
||||
|
@ -162,6 +164,7 @@ export type AlertInfoParams = Pick<
|
|||
RawAlert,
|
||||
| 'params'
|
||||
| 'throttle'
|
||||
| 'notifyWhen'
|
||||
| 'muteAll'
|
||||
| 'mutedInstanceIds'
|
||||
| 'name'
|
||||
|
|
|
@ -156,6 +156,10 @@ describe('alert_form', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
|
|
|
@ -64,6 +64,7 @@ describe('BaseAlert', () => {
|
|||
},
|
||||
tags: [],
|
||||
throttle: '1d',
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -177,6 +177,7 @@ export class BaseAlert {
|
|||
name,
|
||||
alertTypeId,
|
||||
throttle,
|
||||
notifyWhen: null,
|
||||
schedule: { interval },
|
||||
actions: alertActions,
|
||||
},
|
||||
|
|
|
@ -31,5 +31,6 @@ export const createNotifications = async ({
|
|||
enabled,
|
||||
actions: actions.map(transformRuleToAlertAction),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ export const updateNotifications = async ({
|
|||
ruleAlertId,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
} else if (interval && !notification) {
|
||||
|
|
|
@ -404,6 +404,7 @@ export const getResult = (): RuleAlertType => ({
|
|||
enabled: true,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
apiKey: null,
|
||||
|
@ -629,6 +630,7 @@ export const getNotificationResult = (): RuleNotificationAlertType => ({
|
|||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
|
|
|
@ -114,6 +114,7 @@ export const createRules = async ({
|
|||
enabled,
|
||||
actions: actions.map(transformRuleToAlertAction),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -105,6 +105,7 @@ const rule: SanitizedAlert = {
|
|||
enabled: true,
|
||||
actions: [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
apiKeyOwner: 'elastic',
|
||||
|
|
|
@ -172,6 +172,7 @@ export const patchRules = async ({
|
|||
const newRule = {
|
||||
tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
name: calculateName({ updatedName: name, originalName: rule.name }),
|
||||
schedule: {
|
||||
interval: calculateInterval(interval, rule.schedule.interval),
|
||||
|
|
|
@ -74,6 +74,7 @@ export const updateRules = async ({
|
|||
schedule: { interval: ruleUpdate.interval ?? '5m' },
|
||||
actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
};
|
||||
|
||||
const update = await alertsClient.update({
|
||||
|
|
|
@ -143,6 +143,7 @@ export const convertCreateAPIToInternalSchema = (
|
|||
enabled: input.enabled ?? true,
|
||||
actions: input.throttle === 'rule' ? (input.actions ?? []).map(transformRuleToAlertAction) : [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@ export const internalRuleCreate = t.type({
|
|||
actions: actionsCamel,
|
||||
params: ruleParams,
|
||||
throttle: throttleOrNull,
|
||||
notifyWhen: t.null,
|
||||
});
|
||||
export type InternalRuleCreate = t.TypeOf<typeof internalRuleCreate>;
|
||||
|
||||
|
@ -194,6 +195,7 @@ export const internalRuleUpdate = t.type({
|
|||
actions: actionsCamel,
|
||||
params: ruleParams,
|
||||
throttle: throttleOrNull,
|
||||
notifyWhen: t.null,
|
||||
});
|
||||
export type InternalRuleUpdate = t.TypeOf<typeof internalRuleUpdate>;
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
mapFiltersToKql,
|
||||
} from './alert_api';
|
||||
import uuid from 'uuid';
|
||||
import { ALERTS_FEATURE_ID } from '../../../../alerts/common';
|
||||
import { AlertNotifyWhenType, ALERTS_FEATURE_ID } from '../../../../alerts/common';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
|
@ -548,6 +548,7 @@ describe('createAlert', () => {
|
|||
actions: [],
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: 'onActionGroupChange' as AlertNotifyWhenType,
|
||||
createdAt: new Date('1970-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('1970-01-01T00:00:00.000Z'),
|
||||
apiKey: null,
|
||||
|
@ -573,7 +574,7 @@ describe('createAlert', () => {
|
|||
Array [
|
||||
"/api/alerts/alert",
|
||||
Object {
|
||||
"body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}",
|
||||
"body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"notifyWhen\\":\\"onActionGroupChange\\",\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -596,6 +597,7 @@ describe('updateAlert', () => {
|
|||
updatedAt: new Date('1970-01-01T00:00:00.000Z'),
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
notifyWhen: 'onThrottleInterval' as AlertNotifyWhenType,
|
||||
};
|
||||
const resolvedValue: Alert = {
|
||||
...alertToUpdate,
|
||||
|
@ -619,7 +621,7 @@ describe('updateAlert', () => {
|
|||
Array [
|
||||
"/api/alerts/alert/123",
|
||||
Object {
|
||||
"body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[]}",
|
||||
"body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[],\\"notifyWhen\\":\\"onThrottleInterval\\"}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -195,12 +195,15 @@ export async function updateAlert({
|
|||
id,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
alert: Pick<AlertUpdates, 'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions'>;
|
||||
alert: Pick<
|
||||
AlertUpdates,
|
||||
'throttle' | 'name' | 'tags' | 'schedule' | 'params' | 'actions' | 'notifyWhen'
|
||||
>;
|
||||
id: string;
|
||||
}): Promise<Alert> {
|
||||
return await http.put(`${BASE_ALERT_API_PATH}/alert/${id}`, {
|
||||
body: JSON.stringify(
|
||||
pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions'])
|
||||
pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen'])
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -775,6 +775,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -397,6 +397,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -286,6 +286,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -126,6 +126,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -84,6 +84,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -59,6 +59,7 @@ const AlertAdd = ({
|
|||
},
|
||||
actions: [],
|
||||
tags: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
...(initialValues ? initialValues : {}),
|
||||
}),
|
||||
[alertTypeId, consumer, initialValues]
|
||||
|
|
|
@ -103,6 +103,7 @@ describe('alert_edit', () => {
|
|||
tags: [],
|
||||
name: 'test alert',
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
apiKeyOwner: null,
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
|
|
|
@ -378,26 +378,6 @@ describe('alert_form', () => {
|
|||
expect(alertTypeSelectOptions.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update throttle value', async () => {
|
||||
const newThrottle = 17;
|
||||
await setup();
|
||||
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);
|
||||
});
|
||||
|
||||
it('should unset throttle value', async () => {
|
||||
const newThrottle = '';
|
||||
await setup();
|
||||
const throttleField = wrapper.find('[data-test-subj="throttleInput"]');
|
||||
expect(throttleField.exists()).toBeTruthy();
|
||||
throttleField.at(1).simulate('change', { target: { value: newThrottle } });
|
||||
const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]');
|
||||
expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle);
|
||||
});
|
||||
|
||||
it('renders alert type description', async () => {
|
||||
await setup();
|
||||
const alertDescription = wrapper.find('[data-test-subj="alertDescription"]');
|
||||
|
|
|
@ -32,8 +32,6 @@ import {
|
|||
EuiNotificationBadge,
|
||||
EuiErrorBoundary,
|
||||
} from '@elastic/eui';
|
||||
import { some, filter, map, fold } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { capitalize, isObject } from 'lodash';
|
||||
import { KibanaFeature } from '../../../../../features/public';
|
||||
import {
|
||||
|
@ -67,6 +65,7 @@ import './alert_form.scss';
|
|||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { recoveredActionGroupMessage } from '../../constants';
|
||||
import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params';
|
||||
import { AlertNotifyWhen } from './alert_notify_when';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
|
@ -168,7 +167,7 @@ export const AlertForm = ({
|
|||
alert.throttle ? getDurationNumberInItsUnit(alert.throttle) : null
|
||||
);
|
||||
const [alertThrottleUnit, setAlertThrottleUnit] = useState<string>(
|
||||
alert.throttle ? getDurationUnitValue(alert.throttle) : 'm'
|
||||
alert.throttle ? getDurationUnitValue(alert.throttle) : 'h'
|
||||
);
|
||||
const [defaultActionGroupId, setDefaultActionGroupId] = useState<string | undefined>(undefined);
|
||||
const [alertTypesIndex, setAlertTypesIndex] = useState<AlertTypeIndex | null>(null);
|
||||
|
@ -572,22 +571,6 @@ export const AlertForm = ({
|
|||
</>
|
||||
);
|
||||
|
||||
const labelForAlertRenotify = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel"
|
||||
defaultMessage="Notify every"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={i18n.translate('xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip', {
|
||||
defaultMessage: 'Define how often to repeat the action while the alert is active.',
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFlexGrid columns={2}>
|
||||
|
@ -608,7 +591,6 @@ export const AlertForm = ({
|
|||
fullWidth
|
||||
autoFocus={true}
|
||||
isInvalid={errors.name.length > 0 && alert.name !== undefined}
|
||||
compressed
|
||||
name="name"
|
||||
data-test-subj="alertNameInput"
|
||||
value={alert.name || ''}
|
||||
|
@ -633,7 +615,6 @@ export const AlertForm = ({
|
|||
<EuiComboBox
|
||||
noSuggestions
|
||||
fullWidth
|
||||
compressed
|
||||
data-test-subj="tagsComboBox"
|
||||
selectedOptions={tagsOptions}
|
||||
onCreateOption={(searchValue: string) => {
|
||||
|
@ -674,7 +655,6 @@ export const AlertForm = ({
|
|||
fullWidth
|
||||
min={1}
|
||||
isInvalid={errors.interval.length > 0}
|
||||
compressed
|
||||
value={alertInterval || ''}
|
||||
name="interval"
|
||||
data-test-subj="intervalInput"
|
||||
|
@ -689,7 +669,6 @@ export const AlertForm = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
compressed
|
||||
value={alertIntervalUnit}
|
||||
options={getTimeOptions(alertInterval ?? 1)}
|
||||
onChange={(e) => {
|
||||
|
@ -702,52 +681,25 @@ export const AlertForm = ({
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={labelForAlertRenotify}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
compressed
|
||||
value={alertThrottle || ''}
|
||||
name="throttle"
|
||||
data-test-subj="throttleInput"
|
||||
onChange={(e) => {
|
||||
pipe(
|
||||
some(e.target.value.trim()),
|
||||
filter((value) => value !== ''),
|
||||
map((value) => parseInt(value, 10)),
|
||||
filter((value) => !isNaN(value)),
|
||||
fold(
|
||||
() => {
|
||||
// unset throttle
|
||||
setAlertThrottle(null);
|
||||
setAlertProperty('throttle', null);
|
||||
},
|
||||
(throttle) => {
|
||||
setAlertThrottle(throttle);
|
||||
setAlertProperty('throttle', `${throttle}${alertThrottleUnit}`);
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
compressed
|
||||
value={alertThrottleUnit}
|
||||
options={getTimeOptions(alertThrottle ?? 1)}
|
||||
onChange={(e) => {
|
||||
setAlertThrottleUnit(e.target.value);
|
||||
if (alertThrottle) {
|
||||
setAlertProperty('throttle', `${alertThrottle}${e.target.value}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<AlertNotifyWhen
|
||||
alert={alert}
|
||||
throttle={alertThrottle}
|
||||
throttleUnit={alertThrottleUnit}
|
||||
onNotifyWhenChange={useCallback(
|
||||
(notifyWhen) => {
|
||||
setAlertProperty('notifyWhen', notifyWhen);
|
||||
},
|
||||
[setAlertProperty]
|
||||
)}
|
||||
onThrottleChange={useCallback(
|
||||
(throttle: number | null, throttleUnit: string) => {
|
||||
setAlertThrottle(throttle);
|
||||
setAlertThrottleUnit(throttleUnit);
|
||||
setAlertProperty('throttle', throttle ? `${throttle}${throttleUnit}` : null);
|
||||
},
|
||||
[setAlertProperty]
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Alert } from '../../../types';
|
||||
import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
|
||||
import { AlertNotifyWhen } from './alert_notify_when';
|
||||
|
||||
describe('alert_notify_when', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const onNotifyWhenChange = jest.fn();
|
||||
const onThrottleChange = jest.fn();
|
||||
|
||||
describe('action_frequency_form new alert', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
async function setup(overrides = {}) {
|
||||
const initialAlert = ({
|
||||
name: 'test',
|
||||
params: {},
|
||||
consumer: ALERTS_FEATURE_ID,
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
actions: [],
|
||||
tags: [],
|
||||
muteAll: false,
|
||||
enabled: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
...overrides,
|
||||
} as unknown) as Alert;
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<AlertNotifyWhen
|
||||
alert={initialAlert}
|
||||
throttle={null}
|
||||
throttleUnit="m"
|
||||
onNotifyWhenChange={onNotifyWhenChange}
|
||||
onThrottleChange={onThrottleChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
|
||||
it(`should determine initial selection from throttle value if 'notifyWhen' is null`, async () => {
|
||||
await setup({ notifyWhen: null });
|
||||
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
|
||||
expect(notifyWhenSelect.exists()).toBeTruthy();
|
||||
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert');
|
||||
});
|
||||
|
||||
it(`should correctly select 'onActionGroupChange' option on initial render`, async () => {
|
||||
await setup();
|
||||
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
|
||||
expect(notifyWhenSelect.exists()).toBeTruthy();
|
||||
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActionGroupChange');
|
||||
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`should correctly select 'onActiveAlert' option on initial render`, async () => {
|
||||
await setup({
|
||||
notifyWhen: 'onActiveAlert',
|
||||
});
|
||||
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
|
||||
expect(notifyWhenSelect.exists()).toBeTruthy();
|
||||
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert');
|
||||
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`should correctly select 'onThrottleInterval' option on initial render and render throttle inputs`, async () => {
|
||||
await setup({
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
});
|
||||
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
|
||||
expect(notifyWhenSelect.exists()).toBeTruthy();
|
||||
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onThrottleInterval');
|
||||
|
||||
const throttleInput = wrapper.find('[data-test-subj="throttleInput"]');
|
||||
expect(throttleInput.exists()).toBeTruthy();
|
||||
expect(throttleInput.at(1).prop('value')).toEqual(1);
|
||||
|
||||
const throttleUnitInput = wrapper.find('[data-test-subj="throttleUnitInput"]');
|
||||
expect(throttleUnitInput.exists()).toBeTruthy();
|
||||
expect(throttleUnitInput.at(1).prop('value')).toEqual('m');
|
||||
});
|
||||
|
||||
it('should update action frequency type correctly', async () => {
|
||||
await setup();
|
||||
|
||||
wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="onActiveAlert"]').simulate('click');
|
||||
wrapper.update();
|
||||
expect(onNotifyWhenChange).toHaveBeenCalledWith('onActiveAlert');
|
||||
expect(onThrottleChange).toHaveBeenCalledWith(null, 'm');
|
||||
|
||||
wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="onActionGroupChange"]').simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
|
||||
expect(onNotifyWhenChange).toHaveBeenCalledWith('onActionGroupChange');
|
||||
expect(onThrottleChange).toHaveBeenCalledWith(null, 'm');
|
||||
});
|
||||
|
||||
it('should renders throttle input when custom throttle is selected and update throttle value', async () => {
|
||||
await setup({
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
});
|
||||
|
||||
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);
|
||||
expect(onThrottleChange).toHaveBeenCalledWith(17, 'm');
|
||||
|
||||
const newThrottleUnit = 'h';
|
||||
const throttleUnitField = wrapper.find('[data-test-subj="throttleUnitInput"]');
|
||||
expect(throttleUnitField.exists()).toBeTruthy();
|
||||
throttleUnitField.at(1).simulate('change', { target: { value: newThrottleUnit } });
|
||||
expect(onThrottleChange).toHaveBeenCalledWith(null, 'h');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, 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 { InitialAlert } from './alert_reducer';
|
||||
import { getTimeOptions } from '../../../common/lib/get_time_options';
|
||||
import { AlertNotifyWhenType } from '../../../types';
|
||||
|
||||
const DEFAULT_NOTIFY_WHEN_VALUE: AlertNotifyWhenType = 'onActionGroupChange';
|
||||
|
||||
const NOTIFY_WHEN_OPTIONS: Array<EuiSuperSelectOption<AlertNotifyWhenType>> = [
|
||||
{
|
||||
value: 'onActionGroupChange',
|
||||
inputDisplay: 'Run only on status change',
|
||||
'data-test-subj': 'onActionGroupChange',
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Run only on status change"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run when the alert status changes."
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'onActiveAlert',
|
||||
inputDisplay: 'Run every time alert is active',
|
||||
'data-test-subj': 'onActiveAlert',
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Run every time alert is active"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run with every active alert interval."
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'onThrottleInterval',
|
||||
inputDisplay: 'Set a custom action interval',
|
||||
'data-test-subj': 'onThrottleInterval',
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="Set a custom action interval"
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.label"
|
||||
/>
|
||||
</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Actions run using the interval you set."
|
||||
id="xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.description"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
interface AlertNotifyWhenProps {
|
||||
alert: InitialAlert;
|
||||
throttle: number | null;
|
||||
throttleUnit: string;
|
||||
onNotifyWhenChange: (notifyWhen: AlertNotifyWhenType) => void;
|
||||
onThrottleChange: (throttle: number | null, throttleUnit: string) => void;
|
||||
}
|
||||
|
||||
export const AlertNotifyWhen = ({
|
||||
alert,
|
||||
throttle,
|
||||
throttleUnit,
|
||||
onNotifyWhenChange,
|
||||
onThrottleChange,
|
||||
}: AlertNotifyWhenProps) => {
|
||||
const [alertThrottle, setAlertThrottle] = useState<number>(throttle || 1);
|
||||
const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState<boolean>(false);
|
||||
const [notifyWhenValue, setNotifyWhenValue] = useState<AlertNotifyWhenType>(
|
||||
DEFAULT_NOTIFY_WHEN_VALUE
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (alert.notifyWhen) {
|
||||
setNotifyWhenValue(alert.notifyWhen);
|
||||
} else {
|
||||
// If 'notifyWhen' is not set, derive value from existence of throttle value
|
||||
setNotifyWhenValue(alert.throttle ? 'onThrottleInterval' : 'onActiveAlert');
|
||||
}
|
||||
}, [alert]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCustomThrottleOpts(notifyWhenValue === 'onThrottleInterval');
|
||||
}, [notifyWhenValue]);
|
||||
|
||||
const onNotifyWhenValueChange = useCallback((newValue: AlertNotifyWhenType) => {
|
||||
onThrottleChange(newValue === 'onThrottleInterval' ? alertThrottle : null, throttleUnit);
|
||||
onNotifyWhenChange(newValue);
|
||||
setNotifyWhenValue(newValue);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const labelForAlertRenotify = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel"
|
||||
defaultMessage="Notify every"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={i18n.translate('xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip', {
|
||||
defaultMessage: 'Define how often to repeat the action while the alert is active.',
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow fullWidth label={labelForAlertRenotify}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="notifyWhenSelect"
|
||||
options={NOTIFY_WHEN_OPTIONS}
|
||||
valueOfSelected={notifyWhenValue}
|
||||
onChange={onNotifyWhenValueChange}
|
||||
/>
|
||||
{showCustomThrottleOpts && (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
value={alertThrottle}
|
||||
name="throttle"
|
||||
data-test-subj="throttleInput"
|
||||
prepend={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.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) => {
|
||||
setAlertThrottle(value);
|
||||
onThrottleChange(value, throttleUnit);
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
data-test-subj="throttleUnitInput"
|
||||
value={throttleUnit}
|
||||
options={getTimeOptions(throttle ?? 1)}
|
||||
onChange={(e) => {
|
||||
onThrottleChange(throttle, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -18,6 +18,7 @@ describe('alert reducer', () => {
|
|||
},
|
||||
actions: [],
|
||||
tags: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
} as unknown) as Alert;
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { AlertActionParam, IntervalSchedule } from '../../../../../alerts/common
|
|||
import { Alert, AlertAction } from '../../../types';
|
||||
|
||||
export type InitialAlert = Partial<Alert> &
|
||||
Pick<Alert, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>;
|
||||
Pick<Alert, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags' | 'notifyWhen'>;
|
||||
|
||||
interface CommandType<
|
||||
T extends
|
||||
|
|
|
@ -250,6 +250,7 @@ function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
|||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
AlertInstanceStatus,
|
||||
RawAlertInstance,
|
||||
AlertingFrameworkHealth,
|
||||
AlertNotifyWhenType,
|
||||
} from '../../alerts/common';
|
||||
export {
|
||||
Alert,
|
||||
|
@ -30,6 +31,7 @@ export {
|
|||
AlertInstanceStatus,
|
||||
RawAlertInstance,
|
||||
AlertingFrameworkHealth,
|
||||
AlertNotifyWhenType,
|
||||
};
|
||||
export { ActionType };
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ export function getTestAlertData(overwrites = {}) {
|
|||
consumer: 'alertsFixture',
|
||||
schedule: { interval: '1m' },
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [],
|
||||
params: {},
|
||||
...overwrites,
|
||||
|
|
|
@ -115,6 +115,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
|
|||
createdAt: response.body.createdAt,
|
||||
updatedAt: response.body.updatedAt,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
updatedBy: user.username,
|
||||
apiKeyOwner: user.username,
|
||||
muteAll: false,
|
||||
|
|
|
@ -75,6 +75,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
|
|||
createdAt: match.createdAt,
|
||||
updatedAt: match.updatedAt,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
updatedBy: 'elastic',
|
||||
apiKeyOwner: 'elastic',
|
||||
muteAll: false,
|
||||
|
@ -272,6 +273,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
|
|||
apiKeyOwner: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
createdAt: match.createdAt,
|
||||
updatedAt: match.updatedAt,
|
||||
executionStatus: match.executionStatus,
|
||||
|
|
|
@ -71,6 +71,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
updatedAt: response.body.updatedAt,
|
||||
createdAt: response.body.createdAt,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
updatedBy: 'elastic',
|
||||
apiKeyOwner: 'elastic',
|
||||
muteAll: false,
|
||||
|
|
|
@ -73,6 +73,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
},
|
||||
],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -171,6 +172,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '12s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -254,6 +256,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '12s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -348,6 +351,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '12s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -451,6 +455,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '12s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -529,6 +534,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '12s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -798,6 +804,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -863,6 +870,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
@ -938,6 +946,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
schedule: { interval: '1s' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
|
|
|
@ -83,6 +83,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
|
|||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
createdAt: response.body.createdAt,
|
||||
|
|
|
@ -52,6 +52,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
|
|||
scheduledTaskId: match.scheduledTaskId,
|
||||
updatedBy: null,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
createdAt: match.createdAt,
|
||||
|
|
|
@ -46,6 +46,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
createdAt: response.body.createdAt,
|
||||
|
|
|
@ -34,6 +34,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./alerts_space1'));
|
||||
loadTestFile(require.resolve('./alerts_default_space'));
|
||||
loadTestFile(require.resolve('./builtin_alert_types'));
|
||||
loadTestFile(require.resolve('./notify_when'));
|
||||
|
||||
// note that this test will destroy existing spaces
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
|
|
|
@ -91,5 +91,14 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
expect(response.status).to.eql(200);
|
||||
expect(response.body.updatedAt).to.eql('2020-06-17T15:35:39.839Z');
|
||||
});
|
||||
|
||||
it('7.11.0 migrates alerts to contain `notifyWhen` field', async () => {
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e`
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
expect(response.body.notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover, getTestAlertData, getEventLog } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { IValidatedEvent } from '../../../../../plugins/event_log/server';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createNotifyWhenTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('notifyWhen', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
afterEach(async () => await objectRemover.removeAll());
|
||||
|
||||
it(`alert with notifyWhen=onActiveAlert should always execute actions `, async () => {
|
||||
const { body: defaultAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Default Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: recoveredAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Recovered Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const pattern = {
|
||||
instance: [true, true, true, false, true, true],
|
||||
};
|
||||
const expectedActionGroupBasedOnPattern = pattern.instance.map((active: boolean) =>
|
||||
active ? 'default' : 'recovered'
|
||||
);
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.patternFiring',
|
||||
params: { pattern },
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
id: defaultAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: recoveredAction.id,
|
||||
group: 'recovered',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts');
|
||||
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
['execute-action', { gte: 6 }], // one more action (for recovery) will be executed after the last pattern fires
|
||||
['new-instance', { equal: 2 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeActionEvents = getEventsByAction(events, 'execute-action');
|
||||
const executeActionEventsActionGroup = executeActionEvents.map(
|
||||
(event) => event?.kibana?.alerting?.action_group_id
|
||||
);
|
||||
expect(executeActionEventsActionGroup).to.eql(expectedActionGroupBasedOnPattern);
|
||||
});
|
||||
|
||||
it(`alert with notifyWhen=onActionGroupChange should execute actions when action group changes`, async () => {
|
||||
const { body: defaultAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Default Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: recoveredAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Recovered Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const pattern = {
|
||||
instance: [true, true, false, false, true, false],
|
||||
};
|
||||
const expectedActionGroupBasedOnPattern = ['default', 'recovered', 'default', 'recovered'];
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.patternFiring',
|
||||
params: { pattern },
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
actions: [
|
||||
{
|
||||
id: defaultAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: recoveredAction.id,
|
||||
group: 'recovered',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts');
|
||||
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
['execute-action', { gte: 4 }],
|
||||
['new-instance', { equal: 2 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeActionEvents = getEventsByAction(events, 'execute-action');
|
||||
const executeActionEventsActionGroup = executeActionEvents.map(
|
||||
(event) => event?.kibana?.alerting?.action_group_id
|
||||
);
|
||||
expect(executeActionEventsActionGroup).to.eql(expectedActionGroupBasedOnPattern);
|
||||
});
|
||||
|
||||
it(`alert with notifyWhen=onActionGroupChange should only execute actions when action subgroup changes`, async () => {
|
||||
const { body: defaultAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Default Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: recoveredAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Recovered Action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const pattern = {
|
||||
instance: [
|
||||
'subgroup1',
|
||||
'subgroup1',
|
||||
false,
|
||||
false,
|
||||
'subgroup1',
|
||||
'subgroup2',
|
||||
'subgroup2',
|
||||
false,
|
||||
],
|
||||
};
|
||||
const expectedActionGroupBasedOnPattern = [
|
||||
'default',
|
||||
'recovered',
|
||||
'default',
|
||||
'default',
|
||||
'recovered',
|
||||
];
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.patternFiring',
|
||||
params: { pattern },
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
actions: [
|
||||
{
|
||||
id: defaultAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: recoveredAction.id,
|
||||
group: 'recovered',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts');
|
||||
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
provider: 'alerting',
|
||||
actions: new Map([
|
||||
['execute-action', { gte: 5 }],
|
||||
['new-instance', { equal: 2 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeActionEvents = getEventsByAction(events, 'execute-action');
|
||||
const executeActionEventsActionGroup = executeActionEvents.map(
|
||||
(event) => event?.kibana?.alerting?.action_group_id
|
||||
);
|
||||
expect(executeActionEventsActionGroup).to.eql(expectedActionGroupBasedOnPattern);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEventsByAction(events: IValidatedEvent[], action: string) {
|
||||
return events.filter((event) => event?.event?.action === action);
|
||||
}
|
|
@ -54,6 +54,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
apiKeyOwner: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
scheduledTaskId: createdAlert.scheduledTaskId,
|
||||
createdAt: response.body.createdAt,
|
||||
updatedAt: response.body.updatedAt,
|
||||
|
|
|
@ -38,6 +38,8 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) {
|
|||
return testSubjects.setValue('intervalInput', value);
|
||||
},
|
||||
async setAlertThrottleInterval(value: string) {
|
||||
await testSubjects.click('notifyWhenSelect');
|
||||
await testSubjects.click('onThrottleInterval');
|
||||
return testSubjects.setValue('throttleInput', value);
|
||||
},
|
||||
async setAlertExpressionValue(
|
||||
|
|
|
@ -72,6 +72,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const alertName = generateUniqueKey();
|
||||
await defineAlert(alertName);
|
||||
|
||||
await testSubjects.click('notifyWhenSelect');
|
||||
await testSubjects.click('onThrottleInterval');
|
||||
await testSubjects.setValue('throttleInput', '10');
|
||||
|
||||
await testSubjects.click('.slack-ActionTypeSelectOption');
|
||||
await testSubjects.click('addNewActionConnectorButton-.slack');
|
||||
const slackConnectorName = generateUniqueKey();
|
||||
|
|
Loading…
Reference in a new issue