diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 49e7bd1d777432..8794c389d72bcb 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -41,12 +41,14 @@ see https://www.elastic.co/subscriptions[the subscription page]. [float] [[create-connectors]] -=== Connectors +=== Preconfigured connectors and action types You can create connectors for actions in <> or via the action API. For out-of-the-box and standardized connectors, you can <> before {kib} starts. +Action type with only preconfigured connectors could be specified as a <>. + include::action-types/email.asciidoc[] include::action-types/index.asciidoc[] include::action-types/pagerduty.asciidoc[] @@ -54,3 +56,4 @@ include::action-types/server-log.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] +include::pre-configured-action-types.asciidoc[] diff --git a/docs/user/alerting/images/pre-configured-action-type-alert-form.png b/docs/user/alerting/images/pre-configured-action-type-alert-form.png new file mode 100644 index 00000000000000..e12bad468009af Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-alert-form.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-managing.png b/docs/user/alerting/images/pre-configured-action-type-managing.png new file mode 100644 index 00000000000000..95fe1c6aa09586 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-managing.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-select-type.png b/docs/user/alerting/images/pre-configured-action-type-select-type.png new file mode 100644 index 00000000000000..5f555f851cd816 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-select-type.png differ diff --git a/docs/user/alerting/pre-configured-action-types.asciidoc b/docs/user/alerting/pre-configured-action-types.asciidoc new file mode 100644 index 00000000000000..780a2119037b1a --- /dev/null +++ b/docs/user/alerting/pre-configured-action-types.asciidoc @@ -0,0 +1,61 @@ +[role="xpack"] +[[pre-configured-action-types]] + +== Preconfigured action types + +A preconfigure an action type has all the information it needs prior to startup. +A preconfigured action type offers the following capabilities: + +- Requires no setup. Configuration and credentials needed to execute an +action are predefined. +- Has only <>. +- Connectors of the preconfigured action type cannot be edited or deleted. + +[float] +[[preconfigured-action-type-example]] +=== Creating a preconfigured action + +In the `kibana.yml` file: + +. Exclude the action type from `xpack.actions.enabledActionTypes`. +. Add all its connectors. + +The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. + +```js + xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> + xpack.actions.preconfigured: <2> + - id: 'my-server-log' + actionTypeId: .server-log + name: 'Server log #xyz' +``` + +<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. +<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. + +[float] +[[pre-configured-action-type-alert-form]] +=== Attaching a preconfigured action to an alert + +To attach an action to an alert, +select from a list of available action types, and +then select the *Server log* type. This action type was configured previously. + +[role="screenshot"] +image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type] + +[float] +[[managing-pre-configured-action-types]] +=== Managing preconfigured actions + +Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in. +They are tagged as “preconfigured” and cannot be deleted. + +[role="screenshot"] +image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] + +Clicking *Create connector* shows the list of available action types. +Preconfigured action types are not included because you can't create a connector with a preconfigured action type. + +[role="screenshot"] +image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 6a806d1fa531ca..d14d0ca2ddf84a 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -14,6 +14,7 @@ const createActionTypeRegistryMock = () => { list: jest.fn(), ensureActionTypeEnabled: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 26bd68adfc4b66..3be2f265570791 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -28,6 +28,16 @@ beforeEach(() => { ), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, + preconfiguredActions: [ + { + actionTypeId: 'foo', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], }; }); @@ -194,6 +204,19 @@ describe('isActionTypeEnabled', () => { expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); }); + test('should call isActionExecutable of the actions config', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('my-slack1', 'foo'); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has preconfigured connectors', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); + }); + test('should call isLicenseValidForActionType of the license state', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 735ec349837a94..723982b11e1ccd 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType } from './types'; +import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; @@ -17,6 +17,7 @@ export interface ActionTypeRegistryOpts { taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; licenseState: ILicenseState; + preconfiguredActions: PreConfiguredAction[]; } export class ActionTypeRegistry { @@ -25,12 +26,14 @@ export class ActionTypeRegistry { private readonly taskRunnerFactory: TaskRunnerFactory; private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; this.taskRunnerFactory = constructorParams.taskRunnerFactory; this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; + this.preconfiguredActions = constructorParams.preconfiguredActions; } /** @@ -58,6 +61,19 @@ export class ActionTypeRegistry { ); } + /** + * Returns true if action type is enabled or it is a preconfigured action type. + */ + public isActionExecutable(actionId: string, actionTypeId: string) { + return ( + this.isActionTypeEnabled(actionTypeId) || + (!this.isActionTypeEnabled(actionTypeId) && + this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === actionId + ) !== undefined) + ); + } + /** * Registers an action type to the action type registry */ diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 14441bfd52dd73..c96c993fef606f 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -44,6 +44,7 @@ beforeEach(() => { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ @@ -221,6 +222,7 @@ describe('create()', () => { ), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index ac21905ede11c7..9150633f06117f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -27,6 +27,7 @@ export function createActionTypeRegistry(): { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), + preconfiguredActions: [], }); registerBuiltInActionTypes({ logger, diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 6bdd30848e4b73..1b7752588e3d39 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -282,4 +282,65 @@ describe('execute()', () => { }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); + + test('should skip ensure action type if action type is preconfigured and license is valid', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: mockedActionTypeRegistry, + preconfiguredActions: [ + { + actionTypeId: 'mock-action', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], + }); + mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + + await executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }); + expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ + getBasePath: expect.anything(), + headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index ac324587c5f4a0..db38431b02cacd 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -70,7 +70,9 @@ export function createExecuteFunction({ const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest); const actionTypeId = await getActionTypeId(id); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 124e5951c714bb..d6719dc08225e6 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -224,6 +224,50 @@ test('throws an error if actionType is not enabled', async () => { expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test'); }); +test('should not throws an error if actionType is preconfigured', async () => { + const actionType: jest.Mocked = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + config: { + bar: true, + }, + secrets: { + baz: true, + }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { + throw new Error('not enabled for test'); + }); + actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true); + await actionExecutor.execute(executeParams); + + expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0); + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: '1', + services: expect.anything(), + config: { + bar: true, + }, + secrets: { + baz: true, + }, + params: { foo: true }, + }); +}); + test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true }); customActionExecutor.initialize({ diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index ac574decdba715..59f8f3747273f8 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -90,7 +90,9 @@ export class ActionExecutor { namespace.namespace ); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionType = actionTypeRegistry.get(actionTypeId); let validatedParams: Record; diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index bc4268bb698723..95c8b094dfd706 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -20,6 +20,7 @@ const createStartMock = () => { const mock: jest.Mocked = { execute: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), preconfiguredActions: [], }; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 280c14ca8c058e..4c2c8d214f976f 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -65,6 +65,7 @@ export interface PluginSetupContract { export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; + isActionExecutable(actionId: string, actionTypeId: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; preconfiguredActions: PreConfiguredAction[]; @@ -170,6 +171,7 @@ export class ActionsPlugin implements Plugin, Plugi taskManager: plugins.taskManager, actionsConfigUtils, licenseState: this.licenseState, + preconfiguredActions: this.preconfiguredActions, }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; @@ -271,6 +273,9 @@ export class ActionsPlugin implements Plugin, Plugi isActionTypeEnabled: id => { return this.actionTypeRegistry!.isActionTypeEnabled(id); }, + isActionExecutable: (actionId: string, actionTypeId: string) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + }, // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 756080baba6266..0e46ef4919626a 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -51,6 +51,7 @@ const createExecutionHandlerParams = { beforeEach(() => { jest.resetAllMocks(); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); }); test('calls actionsPlugin.execute per selected action', async () => { @@ -111,6 +112,7 @@ test('calls actionsPlugin.execute per selected action', async () => { test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { // Mock two calls, one for check against actions[0] and the second for actions[1] + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); const executionHandler = createExecutionHandler({ @@ -148,6 +150,50 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => }); }); +test('trow error error message when action type is disabled', async () => { + createExecutionHandlerParams.actionsPlugin.preconfiguredActions = []; + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false); + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: '.slack', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + ], + }); + + await executionHandler({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(0); + + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true); + const executionHandlerForPreconfiguredAction = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [...createExecutionHandlerParams.actions], + }); + await executionHandlerForPreconfiguredAction({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); +}); + test('limits actionsPlugin.execute per action group', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 72f9e70905dc28..5c3e36b88879dd 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -71,7 +71,7 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionTypeEnabled(action.actionTypeId)) { + if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 31cc893f785cb1..0f600c7df7bf73 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -185,6 +185,7 @@ describe('Task Runner', () => { test('actionsPlugin.execute is called per alert instance that is scheduled', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( ({ services: executorServices }: AlertExecutorOptions) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 19d43870f4673f..712ceffaef6ed4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15943,7 +15943,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "アクションタイプを読み込み中...", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクション:アクションタイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9875b66e425f8a..7358b381ca7a26 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15948,7 +15948,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "正在加载操作类型……", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "操作:选择操作类型", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts index 9ce50cf47560ac..0a2ec3f203a9ac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts @@ -33,11 +33,20 @@ test('should sort enabled action types first', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[0]); expect(result[1]).toEqual(actionTypes[2]); - expect(result[2]).toEqual(actionTypes[1]); + expect(result[2]).toEqual(actionTypes[3]); + expect(result[3]).toEqual(actionTypes[1]); }); test('should sort by name when all enabled', async () => { @@ -66,9 +75,18 @@ test('should sort by name when all enabled', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[1]); expect(result[1]).toEqual(actionTypes[2]); expect(result[2]).toEqual(actionTypes[0]); + expect(result[3]).toEqual(actionTypes[3]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts index d18cb21b3a0fe0..8078ef4938e50a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts @@ -4,14 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; -export function actionTypeCompare(a: ActionType, b: ActionType) { - if (a.enabled === true && b.enabled === false) { +export function actionTypeCompare( + a: ActionType, + b: ActionType, + preconfiguredConnectors?: ActionConnector[] +) { + const aEnabled = getIsEnabledValue(a, preconfiguredConnectors); + const bEnabled = getIsEnabledValue(b, preconfiguredConnectors); + + if (aEnabled === true && bEnabled === false) { return -1; } - if (a.enabled === false && b.enabled === true) { + if (aEnabled === false && bEnabled === true) { return 1; } return a.name.localeCompare(b.name); } + +const getIsEnabledValue = (actionType: ActionType, preconfiguredConnectors?: ActionConnector[]) => { + let isEnabled = actionType.enabled; + if ( + !actionType.enabledInConfig && + preconfiguredConnectors && + preconfiguredConnectors.length > 0 + ) { + isEnabled = + preconfiguredConnectors.find(connector => connector.actionTypeId === actionType.id) !== + undefined; + } + return isEnabled; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index 566ed7935e0137..9c017aa6fd31fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -4,43 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; -import { checkActionTypeEnabled } from './check_action_type_enabled'; +import { ActionType, ActionConnector } from '../../types'; +import { + checkActionTypeEnabled, + checkActionFormActionTypeEnabled, +} from './check_action_type_enabled'; -test(`returns isEnabled:true when action type isn't provided`, async () => { - expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` +describe('checkActionTypeEnabled', () => { + test(`returns isEnabled:true when action type isn't provided`, async () => { + expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:true when action type is enabled', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:true when action type is enabled', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:false when action type is disabled by license', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: true, - enabledInLicense: false, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:false when action type is disabled by license', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector requires a Basic license.", @@ -63,18 +67,82 @@ test('returns isEnabled:false when action type is disabled by license', async () , } `); + }); + + test('returns isEnabled:false when action type is disabled by config', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This connector is disabled by the Kibana configuration.", + "messageCard": , + } + `); + }); }); -test('returns isEnabled:false when action type is disabled by config', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: false, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` +describe('checkActionFormActionTypeEnabled', () => { + const preconfiguredConnectors: ActionConnector[] = [ + { + actionTypeId: '1', + config: {}, + id: 'test1', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + { + actionTypeId: '2', + config: {}, + id: 'test2', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + ]; + + test('returns isEnabled:true when action type is preconfigured', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); + }); + + test('returns isEnabled:false when action type is disabled by config and not preconfigured', async () => { + const actionType: ActionType = { + id: 'disabled-by-config', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector is disabled by the Kibana configuration.", @@ -85,4 +153,5 @@ test('returns isEnabled:false when action type is disabled by config', async () />, } `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx index 263502a82ec795..971d6dbbb57bf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -9,7 +9,7 @@ import { capitalize } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCard, EuiLink } from '@elastic/eui'; -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants'; import './check_action_type_enabled.scss'; @@ -22,71 +22,98 @@ export interface IsDisabledResult { messageCard: JSX.Element; } +const getLicenseCheckResult = (actionType: ActionType) => { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', + { + defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + }, + } + ), + messageCard: ( + + + + } + /> + ), + }; +}; + +const configurationCheckResult = { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', + { defaultMessage: 'This connector is disabled by the Kibana configuration.' } + ), + messageCard: ( + + ), +}; + export function checkActionTypeEnabled( actionType?: ActionType ): IsEnabledResult | IsDisabledResult { if (actionType?.enabledInLicense === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', - { - defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', - values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), - }, - } - ), - messageCard: ( - - - - } - /> - ), - }; + return getLicenseCheckResult(actionType); } if (actionType?.enabledInConfig === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', - { defaultMessage: 'This connector is disabled by the Kibana configuration.' } - ), - messageCard: ( - - ), - }; + return configurationCheckResult; + } + + return { isEnabled: true }; +} + +export function checkActionFormActionTypeEnabled( + actionType: ActionType, + preconfiguredConnectors: ActionConnector[] +): IsEnabledResult | IsDisabledResult { + if (actionType?.enabledInLicense === false) { + return getLicenseCheckResult(actionType); + } + + if ( + actionType?.enabledInConfig === false && + // do not disable action type if it contains preconfigured connectors (is preconfigured) + !preconfiguredConnectors.find( + preconfiguredConnector => preconfiguredConnector.actionTypeId === actionType.id + ) + ) { + return configurationCheckResult; } return { isEnabled: true }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index d4def86b07b1f7..aed7d18bd9f3d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -73,6 +73,21 @@ describe('action_form', () => { actionParamsFields: null, }; + const preconfiguredOnly = { + id: 'preconfigured', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + describe('action_form in alert', () => { let wrapper: ReactWrapper; @@ -95,6 +110,22 @@ describe('action_form', () => { config: {}, isPreconfigured: true, }, + { + secrets: {}, + id: 'test3', + actionTypeId: preconfiguredOnly.id, + name: 'Preconfigured Only', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + id: 'test4', + actionTypeId: preconfiguredOnly.id, + name: 'Regular connector', + config: {}, + isPreconfigured: false, + }, ]); const mockes = coreMock.createSetup(); deps = { @@ -106,6 +137,7 @@ describe('action_form', () => { actionType, disabledByConfigActionType, disabledByLicenseActionType, + preconfiguredOnly, ]); actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); @@ -166,6 +198,14 @@ describe('action_form', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', }, + { + id: 'preconfigured', + name: 'Preconfigured only', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, { id: 'disabled-by-config', name: 'Disabled by config', @@ -207,21 +247,27 @@ describe('action_form', () => { ).toBeFalsy(); }); - it(`doesn't render action types disabled by config`, async () => { + it('does not render action types disabled by config', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-config-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-config-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeFalsy(); }); - it(`renders available connectors for the selected action type`, async () => { + it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + expect(actionOption.exists()).toBeTruthy(); + }); + + it('renders available connectors for the selected action type', async () => { await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); actionOption.first().simulate('click'); - const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` Array [ Object { @@ -238,10 +284,37 @@ describe('action_form', () => { `); }); + it('renders only preconfigured connectors for the selected preconfigured action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]'); + expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test3", + "key": "test3", + "label": "Preconfigured Only (preconfigured)", + }, + ] + `); + }); + + it('does not render "Add new" button for preconfigured only action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]'); + const addNewConnectorButton = preconfigPannel.find( + '[data-test-subj="addNewActionConnectorButton-preconfigured"]' + ); + expect(addNewConnectorButton.exists()).toBeFalsy(); + }); + it('renders action types disabled by license', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-license-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-license-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeTruthy(); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 4199cfb7b4b7fb..0027837c913d16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -29,7 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; +import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; import { IErrorObject, ActionTypeModel, @@ -42,7 +42,7 @@ import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; -import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; +import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; interface ActionAccordionFormProps { @@ -111,14 +111,12 @@ export const ActionForm = ({ setHasActionsDisabled(hasActionsDisabled); } } catch (e) { - if (toastNotifications) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', - { defaultMessage: 'Unable to load action types' } - ), - }); - } + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); } finally { setIsLoadingActionTypes(false); } @@ -126,41 +124,50 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // load connectors useEffect(() => { - loadConnectors(); + (async () => { + try { + setIsLoadingConnectors(true); + setConnectors(await loadConnectors({ http })); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + { + defaultMessage: 'Unable to load connectors', + } + ), + }); + } finally { + setIsLoadingConnectors(false); + } + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - async function loadConnectors() { - try { - setIsLoadingConnectors(true); - const actionsResponse = await loadAllActions({ http }); - setConnectors(actionsResponse); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', - { - defaultMessage: 'Unable to load connectors', - } - ), - }); - } finally { - setIsLoadingConnectors(false); - } - } const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { defaultMessage: '(preconfigured)', } ); + const getSelectedOptions = (actionItemId: string) => { - const val = connectors.find(connector => connector.id === actionItemId); - if (!val) { + const selectedConnector = connectors.find(connector => connector.id === actionItemId); + if ( + !selectedConnector || + // if selected connector is not preconfigured and action type is for preconfiguration only, + // do not show regular connectors of this type + (actionTypesIndex && + !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig && + !selectedConnector.isPreconfigured) + ) { return []; } - const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`; + const optionTitle = `${selectedConnector.name} ${ + selectedConnector.isPreconfigured ? preconfiguredMessage : '' + }`; return [ { label: optionTitle, @@ -179,8 +186,15 @@ export const ActionForm = ({ }, index: number ) => { + const actionType = actionTypesIndex![actionItem.actionTypeId]; + const optionsList = connectors - .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId) + .filter( + connectorItem => + connectorItem.actionTypeId === actionItem.actionTypeId && + // include only enabled by config connectors or preconfigured + (actionType.enabledInConfig || connectorItem.isPreconfigured) + ) .map(({ name, id, isPreconfigured }) => ({ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, key: id, @@ -189,8 +203,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields; - const checkEnabledResult = checkActionTypeEnabled( - actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId] + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex![actionConnector.actionTypeId], + connectors.filter(connector => connector.isPreconfigured) ); const accordionContent = checkEnabledResult.isEnabled ? ( @@ -211,19 +226,21 @@ export const ActionForm = ({ /> } labelAppend={ - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); - }} - > - - + actionTypesIndex![actionConnector.actionTypeId].enabledInConfig ? ( + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + + + ) : null } > { setActionIdByIndex(selectedOptions[0].id ?? '', index); @@ -258,10 +275,9 @@ export const ActionForm = ({ ); return ( - + -

+

-

+
@@ -349,10 +365,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; return ( - + -

+

-

+
@@ -486,18 +501,26 @@ export const ActionForm = ({ } } - let actionTypeNodes: JSX.Element[] | null = null; + let actionTypeNodes: Array | null = null; let hasDisabledByLicenseActionTypes = false; if (actionTypesIndex) { + const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured); actionTypeNodes = actionTypeRegistry .list() - .filter( - item => actionTypesIndex[item.id] && actionTypesIndex[item.id].enabledInConfig === true + .filter(item => actionTypesIndex[item.id]) + .sort((a, b) => + actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id], preconfiguredConnectors) ) - .sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id])) .map(function(item, index) { const actionType = actionTypesIndex[item.id]; - const checkEnabledResult = checkActionTypeEnabled(actionTypesIndex[item.id]); + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex[item.id], + preconfiguredConnectors + ); + // if action type is not enabled in config and not preconfigured, it shouldn't be displayed + if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) { + return null; + } if (!actionType.enabledInLicense) { hasDisabledByLicenseActionTypes = true; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index bbf8881f0c62a9..597f1ad9119b09 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -68,7 +68,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); await testSubjects.click('.slack-ActionTypeSelectOption'); - await testSubjects.click('createActionConnectorButton'); + await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); await testSubjects.setValue('slackWebhookUrlInput', 'https://test');