[Alerting] Enables AlertTypes to define the custom recovery action groups (#84408)

In this PR we introduce a new `recoveryActionGroup` field on AlertTypes which allows an implementor to specify a custom action group which the framework will use when an alert instance goes from _active_ to _inactive_.
By default all alert types will use the existing `RecoveryActionGroup`, but when `recoveryActionGroup` is specified, this group is used instead.

This is applied across the UI, event log and underlying object model, rather than just being a label change.
To support this we also introduced the `alertActionGroupName` message variable which is the human readable version of existing `alertActionGroup` variable.
This commit is contained in:
Gidi Meir Morris 2020-12-04 13:54:48 +00:00 committed by GitHub
parent b9b2704832
commit 249a1a41aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 576 additions and 151 deletions

View file

@ -43,6 +43,10 @@ export const alertType: AlertType = {
name: 'People In Space Right Now',
actionGroups: [{ id: 'default', name: 'default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: {
id: 'hasLandedBackOnEarth',
name: 'Has landed back on Earth',
},
async executor({ services, params }) {
const { outerSpaceCapacity, craft: craftToTriggerBy, op } = params;

View file

@ -91,6 +91,7 @@ The following table describes the properties of the `options` object.
|name|A user-friendly name for the alert type. These will be displayed in dropdowns when choosing alert types.|string|
|actionGroups|An explicit list of groups the alert type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Alert `actions` validation will use this configuartion to ensure groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the AlertType. |Array<{id:string, name:string}>|
|defaultActionGroupId|Default ID value for the group of the alert type.|string|
|recoveryActionGroup|An action group to use when an alert instance goes from an active state, to an inactive one. This action group should not be specified under the `actionGroups` property. If no recoveryActionGroup is specified, the default `recovered` action group will be used. |{id:string, name:string}|
|actionVariables|An explicit list of action variables the alert type makes available via context and state in action parameter templates, and a short human readable description. Alert UI will use this to display prompts for the users for these variables, in action parameter editors. We highly encourage using `kbn-i18n` to translate the descriptions. |{ context: Array<{name:string, description:string}, state: Array<{name:string, description:string}>|
|validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema|
|executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function|

View file

@ -8,6 +8,7 @@ export interface AlertType {
id: string;
name: string;
actionGroups: ActionGroup[];
recoveryActionGroup: ActionGroup;
actionVariables: string[];
defaultActionGroupId: ActionGroup['id'];
producer: string;

View file

@ -6,13 +6,13 @@
import { i18n } from '@kbn/i18n';
import { ActionGroup } from './alert_type';
export const RecoveredActionGroup: ActionGroup = {
export const RecoveredActionGroup: Readonly<ActionGroup> = {
id: 'recovered',
name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', {
defaultMessage: 'Recovered',
}),
};
export function getBuiltinActionGroups(): ActionGroup[] {
return [RecoveredActionGroup];
export function getBuiltinActionGroups(customRecoveryGroup?: ActionGroup): ActionGroup[] {
return [customRecoveryGroup ?? Object.freeze(RecoveredActionGroup)];
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertType } from '../common';
import { AlertType, RecoveredActionGroup } from '../common';
import { httpServiceMock } from '../../../../src/core/public/mocks';
import { loadAlert, loadAlertType, loadAlertTypes } from './alert_api';
import uuid from 'uuid';
@ -22,6 +22,7 @@ describe('loadAlertTypes', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
},
];
@ -45,6 +46,7 @@ describe('loadAlertType', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
};
http.get.mockResolvedValueOnce([alertType]);
@ -65,6 +67,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
};
http.get.mockResolvedValueOnce([alertType]);
@ -80,6 +83,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
},
]);

View file

@ -5,7 +5,7 @@
*/
import { AlertNavigationRegistry } from './alert_navigation_registry';
import { AlertType, SanitizedAlert } from '../../common';
import { AlertType, RecoveredActionGroup, SanitizedAlert } from '../../common';
import uuid from 'uuid';
beforeEach(() => jest.resetAllMocks());
@ -14,6 +14,7 @@ const mockAlertType = (id: string): AlertType => ({
id,
name: id,
actionGroups: [],
recoveryActionGroup: RecoveredActionGroup,
actionVariables: [],
defaultActionGroupId: 'default',
producer: 'alerts',

View file

@ -122,6 +122,71 @@ describe('register()', () => {
);
});
test('allows an AlertType to specify a custom recovery group', () => {
const alertType = {
id: 'test',
name: 'Test',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
recoveryActionGroup: {
id: 'backToAwesome',
name: 'Back To Awesome',
},
executor: jest.fn(),
producer: 'alerts',
};
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
registry.register(alertType);
expect(registry.get('test').actionGroups).toMatchInlineSnapshot(`
Array [
Object {
"id": "default",
"name": "Default",
},
Object {
"id": "backToAwesome",
"name": "Back To Awesome",
},
]
`);
});
test('throws if the custom recovery group is contained in the AlertType action groups', () => {
const alertType = {
id: 'test',
name: 'Test',
actionGroups: [
{
id: 'default',
name: 'Default',
},
{
id: 'backToAwesome',
name: 'Back To Awesome',
},
],
recoveryActionGroup: {
id: 'backToAwesome',
name: 'Back To Awesome',
},
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerts',
};
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
expect(() => registry.register(alertType)).toThrowError(
new Error(
`Alert type [id="${alertType.id}"] cannot be registered. Action group [backToAwesome] cannot be used as both a recovery and an active action group.`
)
);
});
test('registers the executor with the task manager', () => {
const alertType = {
id: 'test',
@ -243,6 +308,10 @@ describe('get()', () => {
"id": "test",
"name": "Test",
"producer": "alerts",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
}
`);
});
@ -300,6 +369,10 @@ describe('list()', () => {
"id": "test",
"name": "Test",
"producer": "alerts",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);

View file

@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import typeDetect from 'type-detect';
import { intersection } from 'lodash';
import _ from 'lodash';
import { RunContext, TaskManagerSetupContract } from '../../task_manager/server';
import { TaskRunnerFactory } from './task_runner';
import {
@ -18,9 +17,8 @@ import {
AlertTypeState,
AlertInstanceState,
AlertInstanceContext,
ActionGroup,
} from './types';
import { getBuiltinActionGroups } from '../common';
import { RecoveredActionGroup, getBuiltinActionGroups } from '../common';
interface ConstructorOptions {
taskManager: TaskManagerSetupContract;
@ -29,8 +27,13 @@ interface ConstructorOptions {
export interface RegistryAlertType
extends Pick<
AlertType,
'name' | 'actionGroups' | 'defaultActionGroupId' | 'actionVariables' | 'producer'
NormalizedAlertType,
| 'name'
| 'actionGroups'
| 'recoveryActionGroup'
| 'defaultActionGroupId'
| 'actionVariables'
| 'producer'
> {
id: string;
}
@ -55,9 +58,17 @@ const alertIdSchema = schema.string({
},
});
export type NormalizedAlertType<
Params extends AlertTypeParams = AlertTypeParams,
State extends AlertTypeState = AlertTypeState,
InstanceState extends AlertInstanceState = AlertInstanceState,
InstanceContext extends AlertInstanceContext = AlertInstanceContext
> = Omit<AlertType<Params, State, InstanceState, InstanceContext>, 'recoveryActionGroup'> &
Pick<Required<AlertType<Params, State, InstanceState, InstanceContext>>, 'recoveryActionGroup'>;
export class AlertTypeRegistry {
private readonly taskManager: TaskManagerSetupContract;
private readonly alertTypes: Map<string, AlertType> = new Map();
private readonly alertTypes: Map<string, NormalizedAlertType> = new Map();
private readonly taskRunnerFactory: TaskRunnerFactory;
constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) {
@ -86,14 +97,15 @@ export class AlertTypeRegistry {
);
}
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
validateActionGroups(alertType.id, alertType.actionGroups);
alertType.actionGroups = [...alertType.actionGroups, ..._.cloneDeep(getBuiltinActionGroups())];
this.alertTypes.set(alertIdSchema.validate(alertType.id), { ...alertType } as AlertType);
const normalizedAlertType = augmentActionGroupsWithReserved(alertType as AlertType);
this.alertTypes.set(alertIdSchema.validate(alertType.id), normalizedAlertType);
this.taskManager.registerTaskDefinitions({
[`alerting:${alertType.id}`]: {
title: alertType.name,
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create({ ...alertType } as AlertType, context),
this.taskRunnerFactory.create(normalizedAlertType, context),
},
});
}
@ -103,7 +115,7 @@ export class AlertTypeRegistry {
State extends AlertTypeState = AlertTypeState,
InstanceState extends AlertInstanceState = AlertInstanceState,
InstanceContext extends AlertInstanceContext = AlertInstanceContext
>(id: string): AlertType<Params, State, InstanceState, InstanceContext> {
>(id: string): NormalizedAlertType<Params, State, InstanceState, InstanceContext> {
if (!this.has(id)) {
throw Boom.badRequest(
i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', {
@ -114,19 +126,32 @@ export class AlertTypeRegistry {
})
);
}
return this.alertTypes.get(id)! as AlertType<Params, State, InstanceState, InstanceContext>;
return this.alertTypes.get(id)! as NormalizedAlertType<
Params,
State,
InstanceState,
InstanceContext
>;
}
public list(): Set<RegistryAlertType> {
return new Set(
Array.from(this.alertTypes).map(
([id, { name, actionGroups, defaultActionGroupId, actionVariables, producer }]: [
string,
AlertType
]) => ({
([
id,
{
name,
actionGroups,
recoveryActionGroup,
defaultActionGroupId,
actionVariables,
producer,
},
]: [string, NormalizedAlertType]) => ({
id,
name,
actionGroups,
recoveryActionGroup,
defaultActionGroupId,
actionVariables,
producer,
@ -144,21 +169,52 @@ function normalizedActionVariables(actionVariables: AlertType['actionVariables']
};
}
function validateActionGroups(alertTypeId: string, actionGroups: ActionGroup[]) {
const reservedActionGroups = intersection(
actionGroups.map((item) => item.id),
getBuiltinActionGroups().map((item) => item.id)
function augmentActionGroupsWithReserved<
Params extends AlertTypeParams,
State extends AlertTypeState,
InstanceState extends AlertInstanceState,
InstanceContext extends AlertInstanceContext
>(
alertType: AlertType<Params, State, InstanceState, InstanceContext>
): NormalizedAlertType<Params, State, InstanceState, InstanceContext> {
const reservedActionGroups = getBuiltinActionGroups(alertType.recoveryActionGroup);
const { id, actionGroups, recoveryActionGroup } = alertType;
const activeActionGroups = new Set(actionGroups.map((item) => item.id));
const intersectingReservedActionGroups = intersection(
[...activeActionGroups.values()],
reservedActionGroups.map((item) => item.id)
);
if (reservedActionGroups.length > 0) {
if (recoveryActionGroup && activeActionGroups.has(recoveryActionGroup.id)) {
throw new Error(
i18n.translate(
'xpack.alerts.alertTypeRegistry.register.customRecoveryActionGroupUsageError',
{
defaultMessage:
'Alert type [id="{id}"] cannot be registered. Action group [{actionGroup}] cannot be used as both a recovery and an active action group.',
values: {
actionGroup: recoveryActionGroup.id,
id,
},
}
)
);
} else if (intersectingReservedActionGroups.length > 0) {
throw new Error(
i18n.translate('xpack.alerts.alertTypeRegistry.register.reservedActionGroupUsageError', {
defaultMessage:
'Alert type [id="{alertTypeId}"] cannot be registered. Action groups [{actionGroups}] are reserved by the framework.',
'Alert type [id="{id}"] cannot be registered. Action groups [{actionGroups}] are reserved by the framework.',
values: {
actionGroups: reservedActionGroups.join(', '),
alertTypeId,
actionGroups: intersectingReservedActionGroups.join(', '),
id,
},
})
);
}
return {
...alertType,
actionGroups: [...actionGroups, ...reservedActionGroups],
recoveryActionGroup: recoveryActionGroup ?? RecoveredActionGroup,
};
}

View file

@ -14,6 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup, setGlobalDate } from './lib';
import { AlertExecutionStatusValues } from '../../types';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -53,6 +54,7 @@ describe('aggregate()', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myType',
name: 'myType',
producer: 'myApp',
@ -102,6 +104,7 @@ describe('aggregate()', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
authorizedConsumers: {
myApp: { read: true, all: true },

View file

@ -15,6 +15,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -683,6 +684,7 @@ describe('create()', () => {
},
],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
validate: {
params: schema.object({
param1: schema.string(),

View file

@ -15,6 +15,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -52,6 +53,7 @@ describe('find()', () => {
const listedTypes = new Set([
{
actionGroups: [],
recoveryActionGroup: RecoveredActionGroup,
actionVariables: undefined,
defaultActionGroupId: 'default',
id: 'myType',
@ -108,6 +110,7 @@ describe('find()', () => {
id: 'myType',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
producer: 'alerts',
authorizedConsumers: {

View file

@ -9,6 +9,7 @@ import { actionsClientMock } from '../../../../actions/server/mocks';
import { ConstructorOptions } from '../alerts_client';
import { eventLogClientMock } from '../../../../event_log/server/mocks';
import { AlertTypeRegistry } from '../../alert_type_registry';
import { RecoveredActionGroup } from '../../../common';
export const mockedDateString = '2019-02-12T21:01:22.479Z';
@ -82,6 +83,7 @@ export function getBeforeSetup(
id: '123',
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: RecoveredActionGroup,
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerts',

View file

@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { getBeforeSetup } from './lib';
import { RecoveredActionGroup } from '../../../common';
const taskManager = taskManagerMock.createStart();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -50,6 +51,7 @@ describe('listAlertTypes', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'alertingAlertType',
name: 'alertingAlertType',
producer: 'alerts',
@ -58,6 +60,7 @@ describe('listAlertTypes', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -96,6 +99,7 @@ describe('listAlertTypes', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myType',
name: 'myType',
producer: 'myApp',
@ -105,6 +109,7 @@ describe('listAlertTypes', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
},
]);
@ -119,6 +124,7 @@ describe('listAlertTypes', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
producer: 'alerts',
authorizedConsumers: {
myApp: { read: true, all: true },

View file

@ -11,6 +11,7 @@ import { taskManagerMock } from '../../../../task_manager/server/mocks';
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
import { IntervalSchedule, InvalidatePendingApiKey } from '../../types';
import { RecoveredActionGroup } from '../../../common';
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
@ -97,6 +98,7 @@ describe('update()', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
async executor() {},
producer: 'alerts',
});
@ -676,6 +678,7 @@ describe('update()', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
validate: {
params: schema.object({
param1: schema.string(),
@ -1021,6 +1024,7 @@ describe('update()', () => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
async executor() {},
producer: 'alerts',
});

View file

@ -18,6 +18,7 @@ import { ActionsAuthorization } from '../../actions/server';
import { SavedObjectsErrorHelpers } from '../../../../src/core/server';
import { RetryForConflictsAttempts } from './lib/retry_if_conflicts';
import { TaskStatus } from '../../../plugins/task_manager/server/task';
import { RecoveredActionGroup } from '../common';
let alertsClient: AlertsClient;
@ -331,6 +332,7 @@ beforeEach(() => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
async executor() {},
producer: 'alerts',
}));
@ -340,6 +342,7 @@ beforeEach(() => {
name: 'Test',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
async executor() {},
producer: 'alerts',
});

View file

@ -16,6 +16,7 @@ import { AlertsAuthorization, WriteOperations, ReadOperations } from './alerts_a
import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock';
import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger';
import uuid from 'uuid';
import { RecoveredActionGroup } from '../../common';
const alertTypeRegistry = alertTypeRegistryMock.create();
const features: jest.Mocked<FeaturesStartContract> = featuresPluginMock.createStart();
@ -172,6 +173,7 @@ beforeEach(() => {
name: 'My Alert Type',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
async executor() {},
producer: 'myApp',
}));
@ -534,6 +536,7 @@ describe('AlertsAuthorization', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myOtherAppAlertType',
name: 'myOtherAppAlertType',
producer: 'alerts',
@ -542,6 +545,7 @@ describe('AlertsAuthorization', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -550,6 +554,7 @@ describe('AlertsAuthorization', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'mySecondAppAlertType',
name: 'mySecondAppAlertType',
producer: 'myApp',
@ -824,6 +829,7 @@ describe('AlertsAuthorization', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myOtherAppAlertType',
name: 'myOtherAppAlertType',
producer: 'myOtherApp',
@ -832,6 +838,7 @@ describe('AlertsAuthorization', () => {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -880,6 +887,10 @@ describe('AlertsAuthorization', () => {
"id": "myAppAlertType",
"name": "myAppAlertType",
"producer": "myApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
Object {
"actionGroups": Array [],
@ -906,6 +917,10 @@ describe('AlertsAuthorization', () => {
"id": "myOtherAppAlertType",
"name": "myOtherAppAlertType",
"producer": "myOtherApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);
@ -972,6 +987,10 @@ describe('AlertsAuthorization', () => {
"id": "myOtherAppAlertType",
"name": "myOtherAppAlertType",
"producer": "myOtherApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
Object {
"actionGroups": Array [],
@ -994,6 +1013,10 @@ describe('AlertsAuthorization', () => {
"id": "myAppAlertType",
"name": "myAppAlertType",
"producer": "myApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);
@ -1055,6 +1078,10 @@ describe('AlertsAuthorization', () => {
"id": "myAppAlertType",
"name": "myAppAlertType",
"producer": "myApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);
@ -1145,6 +1172,10 @@ describe('AlertsAuthorization', () => {
"id": "myOtherAppAlertType",
"name": "myOtherAppAlertType",
"producer": "myOtherApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
Object {
"actionGroups": Array [],
@ -1167,6 +1198,10 @@ describe('AlertsAuthorization', () => {
"id": "myAppAlertType",
"name": "myAppAlertType",
"producer": "myApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);
@ -1241,6 +1276,10 @@ describe('AlertsAuthorization', () => {
"id": "myOtherAppAlertType",
"name": "myOtherAppAlertType",
"producer": "myOtherApp",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
}
`);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { esKuery } from '../../../../../src/plugins/data/server';
import { RecoveredActionGroup } from '../../common';
import {
asFiltersByAlertTypeAndConsumer,
ensureFieldIsSafeForQuery,
@ -17,6 +18,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => {
{
actionGroups: [],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -40,6 +42,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => {
{
actionGroups: [],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -65,6 +68,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => {
{
actionGroups: [],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
@ -78,6 +82,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => {
{
actionGroups: [],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'myOtherAppAlertType',
name: 'myOtherAppAlertType',
producer: 'alerts',
@ -91,6 +96,7 @@ describe('asFiltersByAlertTypeAndConsumer', () => {
{
actionGroups: [],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
id: 'mySecondAppAlertType',
name: 'mySecondAppAlertType',
producer: 'myApp',

View file

@ -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 { RecoveredActionGroup } from '../../common';
const alertsClient = alertsClientMock.create();
@ -43,6 +44,7 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
authorizedConsumers: {},
actionVariables: {
context: [],
@ -74,6 +76,10 @@ describe('listAlertTypesRoute', () => {
"id": "1",
"name": "name",
"producer": "test",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
],
}
@ -107,6 +113,7 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
authorizedConsumers: {},
actionVariables: {
context: [],
@ -156,6 +163,7 @@ describe('listAlertTypesRoute', () => {
},
],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
authorizedConsumers: {},
actionVariables: {
context: [],

View file

@ -20,6 +20,10 @@ const alertType: AlertType = {
{ id: 'other-group', name: 'Other Group' },
],
defaultActionGroupId: 'default',
recoveryActionGroup: {
id: 'recovered',
name: 'Recovered',
},
executor: jest.fn(),
producer: 'alerts',
};

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { map } from 'lodash';
import { Logger, KibanaRequest } from '../../../../../src/core/server';
import { transformActionParams } from './transform_action_params';
import {
@ -58,7 +57,9 @@ export function createExecutionHandler({
request,
alertParams,
}: CreateExecutionHandlerOptions) {
const alertTypeActionGroups = new Set(map(alertType.actionGroups, 'id'));
const alertTypeActionGroups = new Map(
alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
);
return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => {
if (!alertTypeActionGroups.has(actionGroup)) {
logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`);
@ -76,6 +77,7 @@ export function createExecutionHandler({
tags,
alertInstanceId,
alertActionGroup: actionGroup,
alertActionGroupName: alertTypeActionGroups.get(actionGroup)!,
context,
actionParams: action.params,
state,

View file

@ -33,6 +33,7 @@ const alertType = {
name: 'My test alert',
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
defaultActionGroupId: 'default',
recoveryActionGroup: RecoveredActionGroup,
executor: jest.fn(),
producer: 'alerts',
};
@ -590,6 +591,109 @@ describe('Task Runner', () => {
`);
});
test('fire actions under a custom recovery group when specified on an alert type for alertInstances which are in the recovered state', async () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
const recoveryActionGroup = {
id: 'customRecovered',
name: 'Custom Recovered',
};
const alertTypeWithCustomRecovery = {
...alertType,
recoveryActionGroup,
actionGroups: [{ id: 'default', name: 'Default' }, recoveryActionGroup],
};
alertTypeWithCustomRecovery.executor.mockImplementation(
({ services: executorServices }: AlertExecutorOptions) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
alertTypeWithCustomRecovery,
{
...mockedTaskInstance,
state: {
...mockedTaskInstance.state,
alertInstances: {
'1': { meta: {}, state: { bar: false } },
'2': { meta: {}, state: { bar: false } },
},
},
},
taskRunnerFactoryInitializerParams
);
alertsClient.get.mockResolvedValue({
...mockedAlertTypeSavedObject,
actions: [
{
group: 'default',
id: '1',
actionTypeId: 'action',
params: {
foo: true,
},
},
{
group: recoveryActionGroup.id,
id: '2',
actionTypeId: 'action',
params: {
isResolved: true,
},
},
],
});
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({
id: '1',
type: 'alert',
attributes: {
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
});
const runnerResult = await taskRunner.run();
expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(`
Object {
"1": Object {
"meta": Object {
"lastScheduledActions": Object {
"date": 1970-01-01T00:00:00.000Z,
"group": "default",
},
},
"state": Object {
"bar": false,
},
},
}
`);
const eventLogger = taskRunnerFactoryInitializerParams.eventLogger;
expect(eventLogger.logEvent).toHaveBeenCalledTimes(5);
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2);
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"apiKey": "MTIzOmFiYw==",
"id": "2",
"params": Object {
"isResolved": true,
},
"source": Object {
"source": Object {
"id": "1",
"type": "alert",
},
"type": "SAVED_OBJECT",
},
"spaceId": undefined,
},
]
`);
});
test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => {
alertType.executor.mockImplementation(
({ services: executorServices }: AlertExecutorOptions) => {

View file

@ -20,7 +20,6 @@ import {
ErrorWithReason,
} from '../lib';
import {
AlertType,
RawAlert,
IntervalSchedule,
Services,
@ -39,7 +38,8 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l
import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error';
import { AlertsClient } from '../alerts_client';
import { partiallyUpdateAlert } from '../saved_objects';
import { RecoveredActionGroup } from '../../common';
import { ActionGroup } from '../../common';
import { NormalizedAlertType } from '../alert_type_registry';
const FALLBACK_RETRY_INTERVAL = '5m';
@ -58,10 +58,10 @@ export class TaskRunner {
private context: TaskRunnerContext;
private logger: Logger;
private taskInstance: AlertTaskInstance;
private alertType: AlertType;
private alertType: NormalizedAlertType;
constructor(
alertType: AlertType,
alertType: NormalizedAlertType,
taskInstance: ConcreteTaskInstance,
context: TaskRunnerContext
) {
@ -230,6 +230,7 @@ export class TaskRunner {
if (!muteAll) {
scheduleActionsForRecoveredInstances(
this.alertType.recoveryActionGroup,
alertInstances,
executionHandler,
originalAlertInstances,
@ -499,6 +500,7 @@ function generateNewAndRecoveredInstanceEvents(
}
function scheduleActionsForRecoveredInstances(
recoveryActionGroup: ActionGroup,
alertInstancesMap: Record<string, AlertInstance>,
executionHandler: ReturnType<typeof createExecutionHandler>,
originalAlertInstances: Record<string, AlertInstance>,
@ -514,15 +516,15 @@ function scheduleActionsForRecoveredInstances(
);
for (const id of recoveredIds) {
const instance = alertInstancesMap[id];
instance.updateLastScheduledActions(RecoveredActionGroup.id);
instance.updateLastScheduledActions(recoveryActionGroup.id);
instance.unscheduleActions();
executionHandler({
actionGroup: RecoveredActionGroup.id,
actionGroup: recoveryActionGroup.id,
context: {},
state: {},
alertInstanceId: id,
});
instance.scheduleActions(RecoveredActionGroup.id);
instance.scheduleActions(recoveryActionGroup.id);
}
}

View file

@ -22,6 +22,10 @@ const alertType = {
name: 'My test alert',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: {
id: 'recovered',
name: 'Recovered',
},
executor: jest.fn(),
producer: 'alerts',
};

View file

@ -13,10 +13,11 @@ import {
import { RunContext } from '../../../task_manager/server';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server';
import { AlertType, GetServicesFunction, SpaceIdToNamespaceFunction } from '../types';
import { GetServicesFunction, SpaceIdToNamespaceFunction } from '../types';
import { TaskRunner } from './task_runner';
import { IEventLogger } from '../../../event_log/server';
import { AlertsClient } from '../alerts_client';
import { NormalizedAlertType } from '../alert_type_registry';
export interface TaskRunnerContext {
logger: Logger;
@ -42,7 +43,7 @@ export class TaskRunnerFactory {
this.taskRunnerContext = taskRunnerContext;
}
public create(alertType: AlertType, { taskInstance }: RunContext) {
public create(alertType: NormalizedAlertType, { taskInstance }: RunContext) {
if (!this.isInitialized) {
throw new Error('TaskRunnerFactory not initialized');
}

View file

@ -25,6 +25,7 @@ test('skips non string parameters', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {
foo: 'test',
},
@ -56,6 +57,7 @@ test('missing parameters get emptied out', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -80,6 +82,7 @@ test('context parameters are passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -103,6 +106,7 @@ test('state parameters are passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -126,6 +130,7 @@ test('alertId is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -149,6 +154,7 @@ test('alertName is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -172,6 +178,7 @@ test('tags is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -194,6 +201,7 @@ test('undefined tags is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -217,6 +225,7 @@ test('empty tags is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -240,6 +249,7 @@ test('spaceId is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -263,6 +273,7 @@ test('alertInstanceId is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -286,6 +297,7 @@ test('alertActionGroup is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -295,6 +307,30 @@ test('alertActionGroup is passed to templates', () => {
`);
});
test('alertActionGroupName is passed to templates', () => {
const actionParams = {
message: 'Value "{{alertActionGroupName}}" exists',
};
const result = transformActionParams({
actionParams,
state: {},
context: {},
alertId: '1',
alertName: 'alert-name',
tags: ['tag-A', 'tag-B'],
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
Object {
"message": "Value \\"Action Group\\" exists",
}
`);
});
test('date is passed to templates', () => {
const actionParams = {
message: '{{date}}',
@ -310,6 +346,7 @@ test('date is passed to templates', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
const dateAfter = Date.now();
@ -335,6 +372,7 @@ test('works recursively', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`
@ -362,6 +400,7 @@ test('works recursively with arrays', () => {
spaceId: 'spaceId-A',
alertInstanceId: '2',
alertActionGroup: 'action-group',
alertActionGroupName: 'Action Group',
alertParams: {},
});
expect(result).toMatchInlineSnapshot(`

View file

@ -20,6 +20,7 @@ interface TransformActionParamsOptions {
tags?: string[];
alertInstanceId: string;
alertActionGroup: string;
alertActionGroupName: string;
actionParams: AlertActionParams;
alertParams: AlertTypeParams;
state: AlertInstanceState;
@ -33,6 +34,7 @@ export function transformActionParams({
tags,
alertInstanceId,
alertActionGroup,
alertActionGroupName,
context,
actionParams,
state,
@ -51,6 +53,7 @@ export function transformActionParams({
tags,
alertInstanceId,
alertActionGroup,
alertActionGroupName,
context,
date: new Date().toISOString(),
state,

View file

@ -96,6 +96,7 @@ export interface AlertType<
};
actionGroups: ActionGroup[];
defaultActionGroupId: ActionGroup['id'];
recoveryActionGroup?: ActionGroup;
executor: ({
services,
params,

View file

@ -63,7 +63,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
[field.setValue, actions]
);
const setAlertProperty = useCallback(
const setAlertActionsProperty = useCallback(
(updatedActions: AlertAction[]) => field.setValue(updatedActions),
[field]
);
@ -119,7 +119,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
messageVariables={messageVariables}
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
setActionIdByIndex={setActionIdByIndex}
setAlertProperty={setAlertProperty}
setActions={setAlertActionsProperty}
setActionParamsProperty={setActionParamsProperty}
actionTypeRegistry={actionTypeRegistry}
actionTypes={supportedActionTypes}

View file

@ -32,7 +32,7 @@ export const AddMessageVariables: React.FunctionComponent<Props> = ({
messageVariables?.map((variable: ActionVariable, i: number) => (
<EuiContextMenuItem
key={variable.name}
data-test-subj={`variableMenuButton-${i}`}
data-test-subj={`variableMenuButton-${variable.name}`}
icon="empty"
onClick={() => {
onSelectEventHandler(variable.name);

View file

@ -43,6 +43,10 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
]
`);
});
@ -86,6 +90,10 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "foo-description",
"name": "context.foo",
@ -137,6 +145,10 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "foo-description",
"name": "state.foo",
@ -191,6 +203,10 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "fooC-description",
"name": "context.fooC",
@ -223,6 +239,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType {
actionVariables,
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
authorizedConsumers: {},
producer: ALERTS_FEATURE_ID,
};

View file

@ -87,5 +87,16 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
}),
});
result.push({
name: 'alertActionGroupName',
description: i18n.translate(
'xpack.triggersActionsUI.actionVariables.alertActionGroupNameLabel',
{
defaultMessage:
'The human readable name of the alert action group that was used to scheduled actions for the alert.',
}
),
});
return result;
}

View file

@ -48,6 +48,7 @@ describe('loadAlertTypes', () => {
},
producer: ALERTS_FEATURE_ID,
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
defaultActionGroupId: 'default',
authorizedConsumers: {},
},

View file

@ -10,16 +10,20 @@ import { getDefaultsForActionParams } from './get_defaults_for_action_params';
describe('getDefaultsForActionParams', () => {
test('pagerduty defaults', async () => {
expect(getDefaultsForActionParams('.pagerduty', 'test')).toEqual({
expect(getDefaultsForActionParams(() => false)('.pagerduty', 'test')).toEqual({
dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`,
eventAction: 'trigger',
});
});
test('pagerduty defaults for recovered action group', async () => {
expect(getDefaultsForActionParams('.pagerduty', RecoveredActionGroup.id)).toEqual({
const isRecoveryActionGroup = jest.fn().mockReturnValue(true);
expect(
getDefaultsForActionParams(isRecoveryActionGroup)('.pagerduty', RecoveredActionGroup.id)
).toEqual({
dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`,
eventAction: 'resolve',
});
expect(isRecoveryActionGroup).toHaveBeenCalledWith(RecoveredActionGroup.id);
});
});

View file

@ -4,11 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertActionParam, RecoveredActionGroup } from '../../../../alerts/common';
import { AlertActionParam } from '../../../../alerts/common';
import { EventActionOptions } from '../components/builtin_action_types/types';
import { AlertProvidedActionVariables } from './action_variables';
export type DefaultActionParamsGetter = ReturnType<typeof getDefaultsForActionParams>;
export type DefaultActionParams = ReturnType<DefaultActionParamsGetter>;
export const getDefaultsForActionParams = (
isRecoveryActionGroup: (actionGroupId: string) => boolean
) => (
actionTypeId: string,
actionGroupId: string
): Record<string, AlertActionParam> | undefined => {
@ -18,7 +22,7 @@ export const getDefaultsForActionParams = (
dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`,
eventAction: EventActionOptions.TRIGGER,
};
if (actionGroupId === RecoveredActionGroup.id) {
if (isRecoveryActionGroup(actionGroupId)) {
pagerDutyDefaults.eventAction = EventActionOptions.RESOLVE;
}
return pagerDutyDefaults;

View file

@ -10,9 +10,7 @@ import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
import ActionForm from './action_form';
import { RecoveredActionGroup } from '../../../../../alerts/common';
import { useKibana } from '../../../common/lib/kibana';
import { EuiScreenReaderOnly } from '@elastic/eui';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
@ -212,6 +210,7 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown) as Alert;
const defaultActionMessage = 'Alert [{{context.metadata.name}}] has exceeded the threshold';
const wrapper = mountWithIntl(
<ActionForm
actions={initialAlert.actions}
@ -228,19 +227,18 @@ describe('action_form', () => {
initialAlert.actions[index].id = id;
}}
actionGroups={[
{ id: 'default', name: 'Default' },
{ id: 'default', name: 'Default', defaultActionMessage },
{ id: 'recovered', name: 'Recovered' },
]}
setActionGroupIdByIndex={(group: string, index: number) => {
initialAlert.actions[index].group = group;
}}
setAlertProperty={(_updatedActions: AlertAction[]) => {}}
setActions={(_updatedActions: AlertAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
actionTypeRegistry={actionTypeRegistry}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'}
actionTypes={[
{
id: actionType.id,
@ -356,45 +354,6 @@ describe('action_form', () => {
`);
});
it('renders selected Recovered action group', async () => {
const wrapper = await setup([
{
group: RecoveredActionGroup.id,
id: 'test',
actionTypeId: actionType.id,
params: {
message: '',
},
},
]);
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const actionGroupsSelect = wrapper.find(
`[data-test-subj="addNewActionConnectorActionGroup-0"]`
);
expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "addNewActionConnectorActionGroup-0-option-default",
"inputDisplay": "Default",
"value": "default",
},
Object {
"data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered",
"inputDisplay": "Recovered",
"value": "recovered",
},
]
`);
expect(actionGroupsSelect.first().find(EuiScreenReaderOnly).text()).toEqual(
'Select an option: Recovered, is selected'
);
expect(actionGroupsSelect.first().find('button').first().text()).toEqual('Recovered');
});
it('renders available connectors for the selected action type', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(

View file

@ -37,21 +37,28 @@ import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_en
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
import { ActionGroup, AlertActionParam } from '../../../../../alerts/common';
import { useKibana } from '../../../common/lib/kibana';
import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params';
export interface ActionGroupWithMessageVariables extends ActionGroup {
omitOptionalMessageVariables?: boolean;
defaultActionMessage?: string;
}
export interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
actionGroups?: ActionGroup[];
actionGroups?: ActionGroupWithMessageVariables[];
defaultActionMessage?: string;
setActionIdByIndex: (id: string, index: number) => void;
setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActions: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: AlertActionParam, index: number) => void;
actionTypes?: ActionType[];
messageVariables?: ActionVariables;
defaultActionMessage?: string;
setHasActionsDisabled?: (value: boolean) => void;
setHasActionsWithBrokenConnector?: (value: boolean) => void;
actionTypeRegistry: ActionTypeRegistryContract;
getDefaultActionParams?: DefaultActionParamsGetter;
}
interface ActiveActionConnectorState {
@ -62,17 +69,18 @@ interface ActiveActionConnectorState {
export const ActionForm = ({
actions,
defaultActionGroupId,
actionGroups,
setActionIdByIndex,
setActionGroupIdByIndex,
setAlertProperty,
setActions,
setActionParamsProperty,
actionTypes,
messageVariables,
actionGroups,
defaultActionMessage,
setHasActionsDisabled,
setHasActionsWithBrokenConnector,
actionTypeRegistry,
getDefaultActionParams,
}: ActionAccordionFormProps) => {
const {
http,
@ -303,7 +311,7 @@ export const ActionForm = ({
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setActions(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
.length === 0
@ -333,9 +341,10 @@ export const ActionForm = ({
actionTypesIndex={actionTypesIndex}
connectors={connectors}
defaultActionGroupId={defaultActionGroupId}
defaultActionMessage={defaultActionMessage}
messageVariables={messageVariables}
actionGroups={actionGroups}
defaultActionMessage={defaultActionMessage}
defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)}
setActionGroupIdByIndex={setActionGroupIdByIndex}
onAddConnector={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
@ -349,7 +358,7 @@ export const ActionForm = ({
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setActions(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
0

View file

@ -26,7 +26,8 @@ import {
EuiBadge,
EuiErrorBoundary,
} from '@elastic/eui';
import { AlertActionParam, RecoveredActionGroup } from '../../../../../alerts/common';
import { pick } from 'lodash';
import { AlertActionParam } from '../../../../../alerts/common';
import {
IErrorObject,
AlertAction,
@ -35,14 +36,14 @@ import {
ActionVariables,
ActionVariable,
ActionTypeRegistryContract,
REQUIRED_ACTION_VARIABLES,
} from '../../../types';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionAccordionFormProps } from './action_form';
import { ActionAccordionFormProps, ActionGroupWithMessageVariables } from './action_form';
import { transformActionVariables } from '../../lib/action_variables';
import { recoveredActionGroupMessage } from '../../constants';
import { useKibana } from '../../../common/lib/kibana';
import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params';
import { DefaultActionParams } from '../../lib/get_defaults_for_action_params';
export type ActionTypeFormProps = {
actionItem: AlertAction;
@ -58,6 +59,7 @@ export type ActionTypeFormProps = {
actionTypesIndex: ActionTypeIndex;
connectors: ActionConnector[];
actionTypeRegistry: ActionTypeRegistryContract;
defaultParams: DefaultActionParams;
} & Pick<
ActionAccordionFormProps,
| 'defaultActionGroupId'
@ -92,31 +94,28 @@ export const ActionTypeForm = ({
actionGroups,
setActionGroupIdByIndex,
actionTypeRegistry,
defaultParams,
}: ActionTypeFormProps) => {
const {
application: { capabilities },
} = useKibana().services;
const [isOpen, setIsOpen] = useState(true);
const [availableActionVariables, setAvailableActionVariables] = useState<ActionVariable[]>([]);
const [availableDefaultActionMessage, setAvailableDefaultActionMessage] = useState<
string | undefined
>(undefined);
const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
const selectedActionGroup =
actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
useEffect(() => {
setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group));
const res =
actionItem.group === RecoveredActionGroup.id
? recoveredActionGroupMessage
: defaultActionMessage;
setAvailableDefaultActionMessage(res);
const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group);
if (paramsDefaults) {
for (const [key, paramValue] of Object.entries(paramsDefaults)) {
setAvailableActionVariables(
messageVariables ? getAvailableActionVariables(messageVariables, selectedActionGroup) : []
);
if (defaultParams) {
for (const [key, paramValue] of Object.entries(defaultParams)) {
setActionParamsProperty(key, paramValue, index);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionItem.group]);
}, [actionItem.group, defaultParams]);
const canSave = hasSaveActionsCapability(capabilities);
const getSelectedOptions = (actionItemId: string) => {
@ -167,10 +166,6 @@ export const ActionTypeForm = ({
connectors.filter((connector) => connector.isPreconfigured)
);
const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
const selectedActionGroup =
actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
const accordionContent = checkEnabledResult.isEnabled ? (
<Fragment>
{actionGroups && selectedActionGroup && setActionGroupIdByIndex && (
@ -275,7 +270,7 @@ export const ActionTypeForm = ({
errors={actionParamsErrors.errors}
editAction={setActionParamsProperty}
messageVariables={availableActionVariables}
defaultMessage={availableDefaultActionMessage}
defaultMessage={selectedActionGroup?.defaultActionMessage ?? defaultActionMessage}
actionConnector={actionConnector}
/>
</Suspense>
@ -367,18 +362,12 @@ export const ActionTypeForm = ({
};
function getAvailableActionVariables(
actionVariables: ActionVariables | undefined,
actionGroup: string
actionVariables: ActionVariables,
actionGroup?: ActionGroupWithMessageVariables
) {
if (!actionVariables) {
return [];
}
const filteredActionVariables =
actionGroup === RecoveredActionGroup.id
? { params: actionVariables.params, state: actionVariables.state }
: actionVariables;
return transformActionVariables(filteredActionVariables).sort((a, b) =>
a.name.toUpperCase().localeCompare(b.name.toUpperCase())
);
return transformActionVariables(
actionGroup?.omitOptionalMessageVariables
? pick(actionVariables, ...REQUIRED_ACTION_VARIABLES)
: actionVariables
).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()));
}

View file

@ -47,8 +47,8 @@ const mockAlertApis = {
const authorizedConsumers = {
[ALERTS_FEATURE_ID]: { read: true, all: true },
};
const recoveryActionGroup = { id: 'recovered', name: 'Recovered' };
// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
describe('alert_details', () => {
// mock Api handlers
@ -58,6 +58,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -83,6 +84,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -111,6 +113,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -145,6 +148,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -199,6 +203,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -258,6 +263,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -278,6 +284,7 @@ describe('alert_details', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -307,6 +314,7 @@ describe('disable button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -335,6 +343,7 @@ describe('disable button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -363,6 +372,7 @@ describe('disable button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -400,6 +410,7 @@ describe('disable button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -440,6 +451,7 @@ describe('mute button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -469,6 +481,7 @@ describe('mute button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -498,6 +511,7 @@ describe('mute button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -536,6 +550,7 @@ describe('mute button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -574,6 +589,7 @@ describe('mute button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
@ -639,6 +655,7 @@ describe('edit button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
@ -681,6 +698,7 @@ describe('edit button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
@ -716,6 +734,7 @@ describe('edit button', () => {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',

View file

@ -307,6 +307,7 @@ function mockAlertType(overloads: Partial<AlertType> = {}): AlertType {
params: [],
},
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
authorizedConsumers: {},
producer: 'alerts',
...overloads,

View file

@ -147,6 +147,7 @@ function mockAlertType(overloads: Partial<AlertType> = {}): AlertType {
params: [],
},
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
authorizedConsumers: {},
producer: 'alerts',
...overloads,

View file

@ -61,6 +61,7 @@ describe('alert_add', () => {
},
],
defaultActionGroupId: 'testActionGroup',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },

View file

@ -13,7 +13,7 @@ import { ValidationResult, Alert } from '../../../types';
import { AlertForm } from './alert_form';
import { AlertsContextProvider } from '../../context/alerts_context';
import { coreMock } from 'src/core/public/mocks';
import { ALERTS_FEATURE_ID } from '../../../../../alerts/common';
import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '../../../../../alerts/common';
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
@ -85,6 +85,7 @@ describe('alert_form', () => {
},
],
defaultActionGroupId: 'testActionGroup',
recoveryActionGroup: RecoveredActionGroup,
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
@ -218,6 +219,7 @@ describe('alert_form', () => {
},
],
defaultActionGroupId: 'testActionGroup',
recoveryActionGroup: RecoveredActionGroup,
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },
@ -234,6 +236,7 @@ describe('alert_form', () => {
},
],
defaultActionGroupId: 'testActionGroup',
recoveryActionGroup: RecoveredActionGroup,
producer: 'test',
authorizedConsumers: {
[ALERTS_FEATURE_ID]: { read: true, all: true },

View file

@ -58,6 +58,8 @@ import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/commo
import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities';
import { SolutionFilter } from './solution_filter';
import './alert_form.scss';
import { recoveredActionGroupMessage } from '../../constants';
import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params';
const ENTER_KEY = 13;
@ -306,6 +308,7 @@ export const AlertForm = ({
? !item.alertTypeModel.requiresAppContext
: item.alertType!.producer === alert.consumer
);
const selectedAlertType = alert?.alertTypeId && alertTypesIndex?.get(alert?.alertTypeId);
const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];
@ -461,7 +464,7 @@ export const AlertForm = ({
{AlertParamsExpressionComponent &&
defaultActionGroupId &&
alert.alertTypeId &&
alertTypesIndex?.has(alert.alertTypeId) ? (
selectedAlertType ? (
<EuiErrorBoundary>
<Suspense fallback={<CenterJustifiedSpinner />}>
<AlertParamsExpressionComponent
@ -473,7 +476,7 @@ export const AlertForm = ({
setAlertProperty={setAlertProperty}
alertsContext={alertsContext}
defaultActionGroupId={defaultActionGroupId}
actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
actionGroups={selectedAlertType.actionGroups}
/>
</Suspense>
</EuiErrorBoundary>
@ -482,22 +485,32 @@ export const AlertForm = ({
defaultActionGroupId &&
alertTypeModel &&
alert.alertTypeId &&
alertTypesIndex?.has(alert.alertTypeId) ? (
selectedAlertType ? (
<ActionForm
actions={alert.actions}
setHasActionsDisabled={setHasActionsDisabled}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
messageVariables={alertTypesIndex.get(alert.alertTypeId)!.actionVariables}
messageVariables={selectedAlertType.actionVariables}
defaultActionGroupId={defaultActionGroupId}
actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
actionGroups={selectedAlertType.actionGroups.map((actionGroup) =>
actionGroup.id === selectedAlertType.recoveryActionGroup.id
? {
...actionGroup,
omitOptionalMessageVariables: true,
defaultActionMessage: recoveredActionGroupMessage,
}
: { ...actionGroup, defaultActionMessage: alertTypeModel?.defaultActionMessage }
)}
getDefaultActionParams={getDefaultsForActionParams(
(actionGroupId) => actionGroupId === selectedAlertType.recoveryActionGroup.id
)}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
setActionGroupIdByIndex={(group: string, index: number) =>
setActionProperty('group', group, index)
}
setAlertProperty={setActions}
setActions={setActions}
setActionParamsProperty={setActionParamsProperty}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}
/>
) : null}
</Fragment>

View file

@ -56,6 +56,7 @@ const alertTypeFromApi = {
id: 'test_alert_type',
name: 'some alert type',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,

View file

@ -127,16 +127,19 @@ export interface ActionVariable {
description: string;
}
export interface ActionVariables {
context?: ActionVariable[];
state: ActionVariable[];
params: ActionVariable[];
}
type AsActionVariables<Keys extends string> = {
[Req in Keys]: ActionVariable[];
};
export const REQUIRED_ACTION_VARIABLES = ['state', 'params'] as const;
export const OPTIONAL_ACTION_VARIABLES = ['context'] as const;
export type ActionVariables = AsActionVariables<typeof REQUIRED_ACTION_VARIABLES[number]> &
Partial<AsActionVariables<typeof OPTIONAL_ACTION_VARIABLES[number]>>;
export interface AlertType {
id: string;
name: string;
actionGroups: ActionGroup[];
recoveryActionGroup: ActionGroup;
actionVariables: ActionVariables;
defaultActionGroupId: ActionGroup['id'];
authorizedConsumers: Record<string, { read: boolean; all: boolean }>;

View file

@ -18,6 +18,7 @@ export function defineAlertTypes(
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alertsRestrictedFixture',
defaultActionGroupId: 'default',
recoveryActionGroup: { id: 'restrictedRecovered', name: 'Restricted Recovery' },
async executor({ services, params, state }: AlertExecutorOptions) {},
};
const noopUnrestrictedAlertType: AlertType = {

View file

@ -28,13 +28,21 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
params: [],
},
producer: 'alertsFixture',
recoveryActionGroup: {
id: 'recovered',
name: 'Recovered',
},
};
const expectedRestrictedNoOpType = {
actionGroups: [
{ id: 'default', name: 'Default' },
{ id: 'recovered', name: 'Recovered' },
{ id: 'restrictedRecovered', name: 'Restricted Recovery' },
],
recoveryActionGroup: {
id: 'restrictedRecovered',
name: 'Restricted Recovery',
},
defaultActionGroupId: 'default',
id: 'test.restricted-noop',
name: 'Test: Restricted Noop',

View file

@ -35,6 +35,10 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
params: [],
context: [],
},
recoveryActionGroup: {
id: 'recovered',
name: 'Recovered',
},
producer: 'alertsFixture',
});
expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture');

View file

@ -89,14 +89,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
);
await testSubjects.setValue('messageTextArea', 'test message ');
await testSubjects.click('messageAddVariableButton');
await testSubjects.click('variableMenuButton-0');
await testSubjects.click('variableMenuButton-alertActionGroup');
expect(await messageTextArea.getAttribute('value')).to.eql(
'test message {{alertActionGroup}}'
);
await messageTextArea.type(' some additional text ');
await testSubjects.click('messageAddVariableButton');
await testSubjects.click('variableMenuButton-1');
await testSubjects.click('variableMenuButton-alertId');
expect(await messageTextArea.getAttribute('value')).to.eql(
'test message {{alertActionGroup}} some additional text {{alertId}}'