From ce1836b2af68a49b347f10e8cbdd92f9998ad616 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 12 Mar 2020 23:17:29 -0400 Subject: [PATCH] [Alerting] extend Alert Type with names/descriptions of action variables (#59756) resolves https://github.com/elastic/kibana/issues/58529 This PR extends alertType with an `actionVariables` property, which describes the properties of the context object passed when scheduling actions, and the current state. These property descriptions are used by the web ui for the alert create and edit forms, to allow the properties to be added to action parameters as mustache template variables. --- x-pack/plugins/alerting/README.md | 41 +++- .../server/alert_type_registry.test.ts | 72 +++++++ .../alerting/server/alert_type_registry.ts | 9 + .../task_runner/transform_action_params.ts | 3 + x-pack/plugins/alerting/server/types.ts | 9 + .../index_threshold/action_context.test.ts | 21 +- .../index_threshold/action_context.ts | 10 +- .../index_threshold/alert_type.test.ts | 27 +++ .../alert_types/index_threshold/alert_type.ts | 44 +++++ .../application/lib/action_variables.test.ts | 187 ++++++++++++++++++ .../application/lib/action_variables.ts | 66 +++++++ .../public/application/lib/alert_api.test.ts | 5 +- .../components/alert_details.test.tsx | 32 +-- .../sections/alert_form/alert_form.tsx | 3 +- .../triggers_actions_ui/public/types.ts | 12 +- .../common/fixtures/plugins/alerts/index.ts | 26 +++ .../tests/alerting/list_alert_types.ts | 4 + .../tests/alerting/list_alert_types.ts | 47 ++++- 18 files changed, 575 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index f5d0d2cd071f4..fa2e5c8e2faa1 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -86,6 +86,7 @@ The following table describes the properties of the `options` object. |id|Unique identifier for the alert type. For convention purposes, ids starting with `.` are reserved for built in alert types. We recommend using a convention like `.mySpecialAlert` for your alert types to avoid conflicting with another plugin.|string| |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}>| +|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| @@ -112,11 +113,25 @@ This is the primary function for an alert type. Whenever the alert needs to exec |createdBy|The userid that created this alert.| |updatedBy|The userid that last updated this alert.| +### The `actionVariables` property + +This property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(groupName, context)`. These names are meant to be used in prompters in the alerting user interface, are used as text values for display, and can be inserted into to an action parameter text entry field via UI gesture (eg, clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period). + +For example, if the `context` has one variable `foo` which is an object that has one property `bar`, and there are no `state` variables, the `actionVariables` value would be in the following shape: + +```js +{ + context: [ + { name: 'foo.bar', description: 'the ultra-exciting bar property' }, + ] +} +``` + ### Example This example receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the reading is greater than the threshold. -``` +```typescript import { schema } from '@kbn/config-schema'; ... server.newPlatform.setup.plugins.alerting.registerType({ @@ -128,6 +143,15 @@ server.newPlatform.setup.plugins.alerting.registerType({ threshold: schema.number({ min: 0, max: 1 }), }), }, + actionVariables: { + context: [ + { name: 'server', description: 'the server' }, + { name: 'hasCpuUsageIncreased', description: 'boolean indicating if the cpu usage has increased' }, + ], + state: [ + { name: 'cpuUsage', description: 'CPU usage' }, + ], + }, async executor({ alertId, startedAt, @@ -136,7 +160,8 @@ server.newPlatform.setup.plugins.alerting.registerType({ params, state, }: AlertExecutorOptions) { - const { server, threshold } = params; // Let's assume params is { server: 'server_1', threshold: 0.8 } + // Let's assume params is { server: 'server_1', threshold: 0.8 } + const { server, threshold } = params; // Call a function to get the server's current CPU usage const currentCpuUsage = await getCpuUsage(server); @@ -177,7 +202,7 @@ server.newPlatform.setup.plugins.alerting.registerType({ This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. -``` +```typescript server.newPlatform.setup.plugins.alerting.registerType({ id: 'my-alert-type', name: 'My alert type', @@ -186,6 +211,15 @@ server.newPlatform.setup.plugins.alerting.registerType({ threshold: schema.number({ min: 0, max: 1 }), }), }, + actionVariables: { + context: [ + { name: 'server', description: 'the server' }, + { name: 'hasCpuUsageIncreased', description: 'boolean indicating if the cpu usage has increased' }, + ], + state: [ + { name: 'cpuUsage', description: 'CPU usage' }, + ], + }, async executor({ alertId, startedAt, @@ -446,3 +480,4 @@ The templating system will take the alert and alert type as described above and ``` There are limitations that we are aware of using only templates, and we are gathering feedback and use cases for these. (for example passing an array of strings to an action). + diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index f4749772c7004..b51286281571e 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -6,6 +6,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry } from './alert_type_registry'; +import { AlertType } from './types'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; const taskManager = taskManagerMock.setup(); @@ -126,6 +127,10 @@ describe('get()', () => { "name": "Default", }, ], + "actionVariables": Object { + "context": Array [], + "state": Array [], + }, "defaultActionGroupId": "default", "executor": [MockFunction], "id": "test", @@ -173,6 +178,10 @@ describe('list()', () => { "name": "Test Action Group", }, ], + "actionVariables": Object { + "context": Array [], + "state": Array [], + }, "defaultActionGroupId": "testActionGroup", "id": "test", "name": "Test", @@ -180,4 +189,67 @@ describe('list()', () => { ] `); }); + + test('should return action variables state and empty context', () => { + const registry = new AlertTypeRegistry(alertTypeRegistryParams); + registry.register(alertTypeWithVariables('x', '', 's')); + const alertType = registry.get('x'); + expect(alertType.actionVariables).toBeTruthy(); + + const context = alertType.actionVariables!.context; + const state = alertType.actionVariables!.state; + + expect(context).toBeTruthy(); + expect(context!.length).toBe(0); + + expect(state).toBeTruthy(); + expect(state!.length).toBe(1); + expect(state![0]).toEqual({ name: 's', description: 'x state' }); + }); + + test('should return action variables context and empty state', () => { + const registry = new AlertTypeRegistry(alertTypeRegistryParams); + registry.register(alertTypeWithVariables('x', 'c', '')); + const alertType = registry.get('x'); + expect(alertType.actionVariables).toBeTruthy(); + + const context = alertType.actionVariables!.context; + const state = alertType.actionVariables!.state; + + expect(state).toBeTruthy(); + expect(state!.length).toBe(0); + + expect(context).toBeTruthy(); + expect(context!.length).toBe(1); + expect(context![0]).toEqual({ name: 'c', description: 'x context' }); + }); }); + +function alertTypeWithVariables(id: string, context: string, state: string): AlertType { + const baseAlert = { + id, + name: `${id}-name`, + actionGroups: [], + defaultActionGroupId: id, + executor: (params: any): any => {}, + }; + + if (!context && !state) { + return baseAlert; + } + + const actionVariables = { + context: [{ name: context, description: `${id} context` }], + state: [{ name: state, description: `${id} state` }], + }; + + if (!context) { + delete actionVariables.context; + } + + if (!state) { + delete actionVariables.state; + } + + return { ...baseAlert, actionVariables }; +} diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index d9045fb986745..a2be43f9dacbd 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -40,6 +40,7 @@ export class AlertTypeRegistry { }) ); } + alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); this.alertTypes.set(alertType.id, alertType); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { @@ -71,6 +72,14 @@ export class AlertTypeRegistry { name: alertType.name, actionGroups: alertType.actionGroups, defaultActionGroupId: alertType.defaultActionGroupId, + actionVariables: alertType.actionVariables, })); } } + +function normalizedActionVariables(actionVariables: any) { + return { + context: actionVariables?.context ?? [], + state: actionVariables?.state ?? [], + }; +} diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index f819b02677ce0..e0cff58c4d40a 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -32,6 +32,9 @@ export function transformActionParams({ const result = cloneDeep(actionParams, (value: any) => { if (!isString(value)) return; + // when the list of variables we pass in here changes, + // the UI will need to be updated as well; see: + // x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts const variables = { alertId, alertName, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 635cf0cbd1371..739a0d0aece24 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -52,6 +52,11 @@ export interface AlertExecutorOptions { updatedBy: string | null; } +export interface ActionVariable { + name: string; + description: string; +} + export interface AlertType { id: string; name: string; @@ -61,6 +66,10 @@ export interface AlertType { actionGroups: ActionGroup[]; defaultActionGroupId: ActionGroup['id']; executor: ({ services, params, state }: AlertExecutorOptions) => Promise; + actionVariables?: { + context?: ActionVariable[]; + state?: ActionVariable[]; + }; } export interface RawAlertAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts index d924f5492f88d..a72a7343c5904 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.test.ts @@ -26,11 +26,8 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4], }); - const alertInfo = { - name: '[alert-name]', - }; - const context = addMessages(alertInfo, base, params); - expect(context.subject).toMatchInlineSnapshot( + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` ); expect(context.message).toMatchInlineSnapshot( @@ -57,11 +54,8 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4.2], }); - const alertInfo = { - name: '[alert-name]', - }; - const context = addMessages(alertInfo, base, params); - expect(context.subject).toMatchInlineSnapshot( + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` ); expect(context.message).toMatchInlineSnapshot( @@ -87,11 +81,8 @@ describe('ActionContext', () => { thresholdComparator: 'between', threshold: [4, 5], }); - const alertInfo = { - name: '[alert-name]', - }; - const context = addMessages(alertInfo, base, params); - expect(context.subject).toMatchInlineSnapshot( + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot( `"alert [alert-name] group [group] exceeded threshold"` ); expect(context.message).toMatchInlineSnapshot( diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts index 4a4965db91071..15139ae34c93d 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts @@ -13,9 +13,9 @@ import { AlertExecutorOptions } from '../../../../alerting/server'; type AlertInfo = Pick; export interface ActionContext extends BaseActionContext { - // a short generic message which may be used in an action message - subject: string; - // a longer generic message which may be used in an action message + // a short pre-constructed message which may be used in an action field + title: string; + // a longer pre-constructed message which may be used in an action field message: string; } @@ -34,7 +34,7 @@ export function addMessages( baseContext: BaseActionContext, params: Params ): ActionContext { - const subject = i18n.translate( + const title = i18n.translate( 'xpack.alertingBuiltins.indexThreshold.alertTypeContextSubjectTitle', { defaultMessage: 'alert {name} group {group} exceeded threshold', @@ -65,5 +65,5 @@ export function addMessages( } ); - return { ...baseContext, subject, message }; + return { ...baseContext, title, message }; } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts index 5034b1ee0cd01..5c15c398dbdcd 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.test.ts @@ -22,6 +22,33 @@ describe('alertType', () => { expect(alertType.id).toBe('.index-threshold'); expect(alertType.name).toBe('Index Threshold'); expect(alertType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold Met' }]); + + expect(alertType.actionVariables).toMatchInlineSnapshot(` + Object { + "context": Array [ + Object { + "description": "A pre-constructed message for the alert.", + "name": "message", + }, + Object { + "description": "A pre-constructed title for the alert.", + "name": "title", + }, + Object { + "description": "The group that exceeded the threshold.", + "name": "group", + }, + Object { + "description": "The date the alert exceeded the threshold.", + "name": "date", + }, + Object { + "description": "The value that exceeded the threshold.", + "name": "value", + }, + ], + } + `); }); it('validator succeeds with valid params', async () => { diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts index bc5fcd970bd9b..b79321a8803fa 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts @@ -32,6 +32,41 @@ export function getAlertType(service: Service): AlertType { } ); + const actionVariableContextGroupLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel', + { + defaultMessage: 'The group that exceeded the threshold.', + } + ); + + const actionVariableContextDateLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel', + { + defaultMessage: 'The date the alert exceeded the threshold.', + } + ); + + const actionVariableContextValueLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextValueLabel', + { + defaultMessage: 'The value that exceeded the threshold.', + } + ); + + const actionVariableContextMessageLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextMessageLabel', + { + defaultMessage: 'A pre-constructed message for the alert.', + } + ); + + const actionVariableContextTitleLabel = i18n.translate( + 'xpack.alertingBuiltins.indexThreshold.actionVariableContextTitleLabel', + { + defaultMessage: 'A pre-constructed title for the alert.', + } + ); + return { id: ID, name: alertTypeName, @@ -40,6 +75,15 @@ export function getAlertType(service: Service): AlertType { validate: { params: ParamsSchema, }, + actionVariables: { + context: [ + { name: 'message', description: actionVariableContextMessageLabel }, + { name: 'title', description: actionVariableContextTitleLabel }, + { name: 'group', description: actionVariableContextGroupLabel }, + { name: 'date', description: actionVariableContextDateLabel }, + { name: 'value', description: actionVariableContextValueLabel }, + ], + }, executor, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts new file mode 100644 index 0000000000000..3ed8bc7ba6259 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -0,0 +1,187 @@ +/* + * 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 { AlertType, ActionVariables } from '../../types'; +import { actionVariablesFromAlertType } from './action_variables'; + +beforeEach(() => jest.resetAllMocks()); + +describe('actionVariablesFromAlertType', () => { + test('should return correct variables when no state or context provided', async () => { + const alertType = getAlertType({ context: [], state: [] }); + expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + Array [ + Object { + "description": "The id of the alert.", + "name": "alertId", + }, + Object { + "description": "The name of the alert.", + "name": "alertName", + }, + Object { + "description": "The spaceId of the alert.", + "name": "spaceId", + }, + Object { + "description": "The tags of the alert.", + "name": "tags", + }, + Object { + "description": "The alert instance id that scheduled actions for the alert.", + "name": "alertInstanceId", + }, + ] + `); + }); + + test('should return correct variables when no state provided', async () => { + const alertType = getAlertType({ + context: [ + { name: 'foo', description: 'foo-description' }, + { name: 'bar', description: 'bar-description' }, + ], + state: [], + }); + expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + Array [ + Object { + "description": "The id of the alert.", + "name": "alertId", + }, + Object { + "description": "The name of the alert.", + "name": "alertName", + }, + Object { + "description": "The spaceId of the alert.", + "name": "spaceId", + }, + Object { + "description": "The tags of the alert.", + "name": "tags", + }, + Object { + "description": "The alert instance id that scheduled actions for the alert.", + "name": "alertInstanceId", + }, + Object { + "description": "foo-description", + "name": "context.foo", + }, + Object { + "description": "bar-description", + "name": "context.bar", + }, + ] + `); + }); + + test('should return correct variables when no context provided', async () => { + const alertType = getAlertType({ + context: [], + state: [ + { name: 'foo', description: 'foo-description' }, + { name: 'bar', description: 'bar-description' }, + ], + }); + expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + Array [ + Object { + "description": "The id of the alert.", + "name": "alertId", + }, + Object { + "description": "The name of the alert.", + "name": "alertName", + }, + Object { + "description": "The spaceId of the alert.", + "name": "spaceId", + }, + Object { + "description": "The tags of the alert.", + "name": "tags", + }, + Object { + "description": "The alert instance id that scheduled actions for the alert.", + "name": "alertInstanceId", + }, + Object { + "description": "foo-description", + "name": "state.foo", + }, + Object { + "description": "bar-description", + "name": "state.bar", + }, + ] + `); + }); + + test('should return correct variables when both context and state provided', async () => { + const alertType = getAlertType({ + context: [ + { name: 'fooC', description: 'fooC-description' }, + { name: 'barC', description: 'barC-description' }, + ], + state: [ + { name: 'fooS', description: 'fooS-description' }, + { name: 'barS', description: 'barS-description' }, + ], + }); + expect(actionVariablesFromAlertType(alertType)).toMatchInlineSnapshot(` + Array [ + Object { + "description": "The id of the alert.", + "name": "alertId", + }, + Object { + "description": "The name of the alert.", + "name": "alertName", + }, + Object { + "description": "The spaceId of the alert.", + "name": "spaceId", + }, + Object { + "description": "The tags of the alert.", + "name": "tags", + }, + Object { + "description": "The alert instance id that scheduled actions for the alert.", + "name": "alertInstanceId", + }, + Object { + "description": "fooC-description", + "name": "context.fooC", + }, + Object { + "description": "barC-description", + "name": "context.barC", + }, + Object { + "description": "fooS-description", + "name": "state.fooS", + }, + Object { + "description": "barS-description", + "name": "state.barS", + }, + ] + `); + }); +}); + +function getAlertType(actionVariables: ActionVariables): AlertType { + return { + id: 'test', + name: 'Test', + actionVariables, + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts new file mode 100644 index 0000000000000..8a2d22372cdb3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -0,0 +1,66 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { AlertType, ActionVariable } from '../../types'; + +// return a "flattened" list of action variables for an alertType +export function actionVariablesFromAlertType(alertType: AlertType): ActionVariable[] { + const alwaysProvidedVars = getAlwaysProvidedActionVariables(); + const contextVars = prefixKeys(alertType.actionVariables.context, 'context.'); + const stateVars = prefixKeys(alertType.actionVariables.state, 'state.'); + + return alwaysProvidedVars.concat(contextVars, stateVars); +} + +function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVariable[] { + return actionVariables.map(actionVariable => { + return { name: `${prefix}${actionVariable.name}`, description: actionVariable.description }; + }); +} + +// this list should be the same as in: +// x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +function getAlwaysProvidedActionVariables(): ActionVariable[] { + const result: ActionVariable[] = []; + + result.push({ + name: 'alertId', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.alertIdLabel', { + defaultMessage: 'The id of the alert.', + }), + }); + + result.push({ + name: 'alertName', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.alertNameLabel', { + defaultMessage: 'The name of the alert.', + }), + }); + + result.push({ + name: 'spaceId', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.spaceIdLabel', { + defaultMessage: 'The spaceId of the alert.', + }), + }); + + result.push({ + name: 'tags', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.tagsLabel', { + defaultMessage: 'The tags of the alert.', + }), + }); + + result.push({ + name: 'alertInstanceId', + description: i18n.translate('xpack.triggersActionsUI.actionVariables.alertInstanceIdLabel', { + defaultMessage: 'The alert instance id that scheduled actions for the alert.', + }), + }); + + return result; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index ebbfb0fc4b76f..0b06982828446 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -38,7 +38,10 @@ describe('loadAlertTypes', () => { { id: 'test', name: 'Test', - actionVariables: ['var1'], + actionVariables: { + context: [{ name: 'var1', description: 'val1' }], + state: [{ name: 'var2', description: 'val2' }], + }, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 64069009f6589..c142f0c6d3a50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -53,8 +53,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; expect( @@ -86,8 +86,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; expect( @@ -114,8 +114,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const actionTypes: ActionType[] = [ @@ -164,8 +164,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const actionTypes: ActionType[] = [ { @@ -215,8 +215,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; expect( @@ -240,8 +240,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; expect( @@ -265,8 +265,8 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; expect( @@ -295,8 +295,8 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableButton = shallow( @@ -321,8 +321,8 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableButton = shallow( @@ -347,8 +347,8 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const disableAlert = jest.fn(); @@ -382,8 +382,8 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableAlert = jest.fn(); @@ -420,8 +420,8 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableButton = shallow( @@ -447,8 +447,8 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableButton = shallow( @@ -474,8 +474,8 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const muteAlert = jest.fn(); @@ -510,8 +510,8 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const unmuteAlert = jest.fn(); @@ -546,8 +546,8 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - actionVariables: [], }; const enableButton = shallow( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index f90a05e2b766e..aa56b565ef324 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -25,6 +25,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { loadAlertTypes } from '../../lib/alert_api'; +import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; @@ -213,7 +214,7 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: actions={alert.actions} messageVariables={ alertTypesIndex && alertTypesIndex[alert.alertTypeId] - ? alertTypesIndex[alert.alertTypeId].actionVariables + ? actionVariablesFromAlertType(alertTypesIndex[alert.alertTypeId]).map(av => av.name) : undefined } defaultActionGroupId={defaultActionGroupId} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2119c08bedc31..d9681e2474f00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -71,11 +71,21 @@ export interface ActionConnectorTableItem extends ActionConnector { actionType: ActionType['name']; } +export interface ActionVariable { + name: string; + description: string; +} + +export interface ActionVariables { + context: ActionVariable[]; + state: ActionVariable[]; +} + export interface AlertType { id: string; name: string; actionGroups: ActionGroup[]; - actionVariables: string[]; + actionVariables: ActionVariables; defaultActionGroupId: ActionGroup['id']; } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 2e7674f2b3eb7..58f7a49720007 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -207,6 +207,10 @@ export default function(kibana: any) { { id: 'other', name: 'Other' }, ], defaultActionGroupId: 'default', + actionVariables: { + state: [{ name: 'instanceStateValue', description: 'the instance state value' }], + context: [{ name: 'instanceContextValue', description: 'the instance context value' }], + }, async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, @@ -419,6 +423,26 @@ export default function(kibana: any) { defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) {}, }; + const onlyContextVariablesAlertType: AlertType = { + id: 'test.onlyContextVariables', + name: 'Test: Only Context Variables', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'aContextVariable', description: 'this is a context variable' }], + }, + async executor(opts: AlertExecutorOptions) {}, + }; + const onlyStateVariablesAlertType: AlertType = { + id: 'test.onlyStateVariables', + name: 'Test: Only State Variables', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + actionVariables: { + state: [{ name: 'aStateVariable', description: 'this is a state variable' }], + }, + async executor(opts: AlertExecutorOptions) {}, + }; server.newPlatform.setup.plugins.alerting.registerType(alwaysFiringAlertType); server.newPlatform.setup.plugins.alerting.registerType(cumulativeFiringAlertType); server.newPlatform.setup.plugins.alerting.registerType(neverFiringAlertType); @@ -426,6 +450,8 @@ export default function(kibana: any) { server.newPlatform.setup.plugins.alerting.registerType(validationAlertType); server.newPlatform.setup.plugins.alerting.registerType(authorizationAlertType); server.newPlatform.setup.plugins.alerting.registerType(noopAlertType); + server.newPlatform.setup.plugins.alerting.registerType(onlyContextVariablesAlertType); + server.newPlatform.setup.plugins.alerting.registerType(onlyStateVariablesAlertType); }, }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 30c1548b7db2a..3f86214517685 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -44,6 +44,10 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { defaultActionGroupId: 'default', id: 'test.noop', name: 'Test: Noop', + actionVariables: { + state: [], + context: [], + }, }); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 590de1ea7ce0b..fbcf744b96916 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -16,7 +16,6 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { describe('list_alert_types', () => { it('should return 200 with list of alert types', async () => { const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); - expect(response.statusCode).to.eql(200); const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ @@ -24,6 +23,52 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { defaultActionGroupId: 'default', id: 'test.noop', name: 'Test: Noop', + actionVariables: { + state: [], + context: [], + }, + }); + }); + + it('should return actionVariables with both context and state', async () => { + const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + expect(response.statusCode).to.eql(200); + + const fixtureAlertType = response.body.find( + (alertType: any) => alertType.id === 'test.always-firing' + ); + + expect(fixtureAlertType.actionVariables).to.eql({ + state: [{ name: 'instanceStateValue', description: 'the instance state value' }], + context: [{ name: 'instanceContextValue', description: 'the instance context value' }], + }); + }); + + it('should return actionVariables with just context', async () => { + const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + expect(response.statusCode).to.eql(200); + + const fixtureAlertType = response.body.find( + (alertType: any) => alertType.id === 'test.onlyContextVariables' + ); + + expect(fixtureAlertType.actionVariables).to.eql({ + state: [], + context: [{ name: 'aContextVariable', description: 'this is a context variable' }], + }); + }); + + it('should return actionVariables with just state', async () => { + const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + expect(response.statusCode).to.eql(200); + + const fixtureAlertType = response.body.find( + (alertType: any) => alertType.id === 'test.onlyStateVariables' + ); + + expect(fixtureAlertType.actionVariables).to.eql({ + state: [{ name: 'aStateVariable', description: 'this is a state variable' }], + context: [], }); }); });