diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index ef0bb92dd3cb9..9d6cd8db56084 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -41589,10 +41589,7 @@ "xpack.stackConnectors.components.slack_api.error.requiredSlackMessageText": "Nachricht ist erforderlich.", "xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired": "JSON muss das Feld „blocks“ enthalten.", "xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired": "Das Block-Kit muss ein gültiges JSON sein.", - "xpack.stackConnectors.components.slack_api.errorInvalidChannelsText": "Die Kanal-ID \"{channels}\" kann nicht validiert werden. Bitte überprüfen Sie die Gültigkeit Ihres Tokens und/oder der Kanal-ID.", - "xpack.stackConnectors.components.slack_api.errorValidChannelsText": "Kanäle können nicht validiert werden, bitte überprüfen Sie die Gültigkeit Ihres Tokens oder Ihres Kanals", "xpack.stackConnectors.components.slack_api.selectMessageText": "Senden Sie Nachrichten an Slack-Kanäle.", - "xpack.stackConnectors.components.slack_api.successFetchChannelsText": "Alle Kanäle abrufen", "xpack.stackConnectors.components.slack_api.tokenTextFieldLabel": "API-Token", "xpack.stackConnectors.components.slack_api.webApi": "Web-API", "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "Nachricht ist erforderlich.", @@ -41868,9 +41865,7 @@ "xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage": "Fehler beim Posten einer Slack-Nachricht – versuchen Sie es erneut um {retryString}", "xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage": "Fehler beim Posten einer Slack-Nachricht, bitte später erneut versuchen", "xpack.stackConnectors.slack.params.blockkitLabel": "Block-Kit", - "xpack.stackConnectors.slack.params.channelIdComboBoxLabel": "Kanal-ID", "xpack.stackConnectors.slack.params.channelsComboBoxLabel": "Kanal", - "xpack.stackConnectors.slack.params.componentError.validChannelsRequestFailed": "{validChannelId} ist kein gültiger Slack-Kanal.", "xpack.stackConnectors.slack.params.textLabel": "Text", "xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage": "unerwartete HTTP-Reaktion von Slack: {httpStatus} {httpStatusText}", "xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage": "unerwartete Null-Reaktion von Slack", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 5a50a75703e68..0615c4efdcb85 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -41996,10 +41996,7 @@ "xpack.stackConnectors.components.slack_api.error.requiredSlackMessageText": "Le message est requis.", "xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired": "L'objet JSON doit contenir des \"blocs\" de champs.", "xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired": "Le kit de blocs doit être un objet JSON valide.", - "xpack.stackConnectors.components.slack_api.errorInvalidChannelsText": "Impossible de valider l'ID \"{channels}\" du canal. Nous vous invitons à vérifier la validité de votre token ou l'ID du canal", - "xpack.stackConnectors.components.slack_api.errorValidChannelsText": "Impossible de valider les canaux, veuillez vérifier la validité de votre token ou de votre canal", "xpack.stackConnectors.components.slack_api.selectMessageText": "Envoyer des messages aux canaux Slack.", - "xpack.stackConnectors.components.slack_api.successFetchChannelsText": "Récupérer tous les canaux", "xpack.stackConnectors.components.slack_api.tokenTextFieldLabel": "Token d'API", "xpack.stackConnectors.components.slack_api.webApi": "API web", "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "Le message est requis.", @@ -42272,9 +42269,7 @@ "xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage": "erreur lors de la publication d'un message slack, réessayer à cette date/heure : {retryString}", "xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage": "erreur lors de la publication d'un message slack, réessayer ultérieurement", "xpack.stackConnectors.slack.params.blockkitLabel": "Kit de blocs", - "xpack.stackConnectors.slack.params.channelIdComboBoxLabel": "ID canal", "xpack.stackConnectors.slack.params.channelsComboBoxLabel": "Canal", - "xpack.stackConnectors.slack.params.componentError.validChannelsRequestFailed": "{validChannelId} n'est pas un canal Slack valide", "xpack.stackConnectors.slack.params.textLabel": "Texte", "xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage": "réponse http inattendue de Slack : {httpStatus} {httpStatusText}", "xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage": "réponse nulle inattendue de Slack", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 18357f4d1404e..18bfa60c3aeee 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -42037,10 +42037,7 @@ "xpack.stackConnectors.components.slack_api.error.requiredSlackMessageText": "メッセージが必要です。", "xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired": "JSONには\"blocks\"フィールドを含める必要があります。", "xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired": "Block kitは有効なJSONでなければなりません。", - "xpack.stackConnectors.components.slack_api.errorInvalidChannelsText": "チャンネルID \"{channels}\"を検証できません。トークンまたはチャンネルIDの有効性を確認してください。", - "xpack.stackConnectors.components.slack_api.errorValidChannelsText": "有効なチャンネルではありません。トークンまたはチャンネルの有効期限を確認してください", "xpack.stackConnectors.components.slack_api.selectMessageText": "メッセージをSlackチャンネルに送信します。", - "xpack.stackConnectors.components.slack_api.successFetchChannelsText": "すべてのチャンネルを取得", "xpack.stackConnectors.components.slack_api.tokenTextFieldLabel": "APIトークン", "xpack.stackConnectors.components.slack_api.webApi": "Web API", "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "メッセージが必要です。", @@ -42313,9 +42310,7 @@ "xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} で再試行", "xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage": "slack メッセージの投稿エラー、後ほど再試行", "xpack.stackConnectors.slack.params.blockkitLabel": "Block Kit", - "xpack.stackConnectors.slack.params.channelIdComboBoxLabel": "チャンネルID", "xpack.stackConnectors.slack.params.channelsComboBoxLabel": "チャンネル", - "xpack.stackConnectors.slack.params.componentError.validChannelsRequestFailed": "{validChannelId}は有効なSlackチャンネルではありません。", "xpack.stackConnectors.slack.params.textLabel": "Text", "xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage": "slack からの予期せぬ http 応答:{httpStatus} {httpStatusText}", "xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage": "Slack から予期せぬ null 応答", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index bcd0e2f7918d0..58deb890e53dc 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -42018,10 +42018,7 @@ "xpack.stackConnectors.components.slack_api.error.requiredSlackMessageText": "“消息”必填。", "xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired": "JSON 必须包含字段“块”。", "xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired": "Block Kit 必须为有效 JSON。", - "xpack.stackConnectors.components.slack_api.errorInvalidChannelsText": "无法验证频道 ID“{channels}”,请检查您的令牌和/或频道 ID 的有效性", - "xpack.stackConnectors.components.slack_api.errorValidChannelsText": "无法使频道生效,请检查您的令牌或频道的有效性", "xpack.stackConnectors.components.slack_api.selectMessageText": "向 Slack 频道发送消息。", - "xpack.stackConnectors.components.slack_api.successFetchChannelsText": "提取所有频道", "xpack.stackConnectors.components.slack_api.tokenTextFieldLabel": "API 令牌", "xpack.stackConnectors.components.slack_api.webApi": "Web API", "xpack.stackConnectors.components.slack..error.requiredSlackMessageText": "“消息”必填。", @@ -42294,9 +42291,7 @@ "xpack.stackConnectors.slack.errorPostingRetryDateErrorMessage": "发布 Slack 消息时出错,在 {retryString} 重试", "xpack.stackConnectors.slack.errorPostingRetryLaterErrorMessage": "发布 slack 消息时出错,稍后重试", "xpack.stackConnectors.slack.params.blockkitLabel": "Block Kit", - "xpack.stackConnectors.slack.params.channelIdComboBoxLabel": "频道 ID", "xpack.stackConnectors.slack.params.channelsComboBoxLabel": "频道", - "xpack.stackConnectors.slack.params.componentError.validChannelsRequestFailed": "{validChannelId} 不是有效的 Slack 频道", "xpack.stackConnectors.slack.params.textLabel": "文本", "xpack.stackConnectors.slack.unexpectedHttpResponseErrorMessage": "来自 slack 的非预期 http 响应:{httpStatus} {httpStatusText}", "xpack.stackConnectors.slack.unexpectedNullResponseErrorMessage": "来自 slack 的异常空响应", diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx new file mode 100644 index 0000000000000..ad5e047d37771 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; +import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { registerConnectorTypes } from '..'; +import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks'; +import { CONNECTOR_ID } from '@kbn/connector-schemas/slack_api/constants'; +import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; + +let connectorTypeModel: ConnectorTypeModel; + +beforeAll(async () => { + const connectorTypeRegistry = new TypeRegistry(); + ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock }); + registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock }); + + const getResult = connectorTypeRegistry.get(CONNECTOR_ID); + + if (getResult !== null) { + connectorTypeModel = getResult; + } +}); + +describe('deserializer', () => { + it('should deserialize the config as expected', () => { + const data = { + id: '1', + name: 'slack api', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + actionTypeId: CONNECTOR_ID, + config: { allowedChannels: [{ name: 'general' }, { name: '#random' }] }, + secrets: {}, + }; + + const deserializer = connectorTypeModel.connectorForm?.deserializer!; + const result = deserializer(data); + + expect(result).toEqual({ + ...data, + config: { + ...data.config, + allowedChannels: ['#general', '#random'], + }, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts new file mode 100644 index 0000000000000..5226204eb6dff --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_deserializer.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ConnectorFormSchema, InternalConnectorForm } from '@kbn/alerts-ui-shared'; +import type { SlackApiConfig } from '@kbn/connector-schemas/slack_api'; + +export const deserializer = (data: ConnectorFormSchema): InternalConnectorForm => { + const allowedChannels = (data.config?.allowedChannels as SlackApiConfig['allowedChannels']) ?? []; + + const formattedChannels = + allowedChannels.map((channel) => { + if (channel.name.startsWith('#')) { + return channel.name; + } + + return `#${channel.name}`; + }) ?? []; + + return { + ...data, + config: { ...data.config, allowedChannels: formattedChannels }, + }; +}; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx new file mode 100644 index 0000000000000..acd6107873cb2 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; +import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { registerConnectorTypes } from '..'; +import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks'; +import { CONNECTOR_ID } from '@kbn/connector-schemas/slack_api/constants'; +import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; + +let connectorTypeModel: ConnectorTypeModel; + +beforeAll(async () => { + const connectorTypeRegistry = new TypeRegistry(); + ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock }); + registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock }); + + const getResult = connectorTypeRegistry.get(CONNECTOR_ID); + + if (getResult !== null) { + connectorTypeModel = getResult; + } +}); + +describe('serializer', () => { + it('should serialize the config as expected', () => { + const data = { + id: '1', + name: 'slack api', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + actionTypeId: CONNECTOR_ID, + config: { allowedChannels: ['#general', '#random'] }, + secrets: {}, + }; + + const serializer = connectorTypeModel.connectorForm?.serializer!; + + const result = serializer(data); + + expect(result).toEqual({ + ...data, + config: { + ...data.config, + allowedChannels: [{ name: '#general' }, { name: '#random' }], + }, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts new file mode 100644 index 0000000000000..7467b79116f42 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/form_serializer.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ConnectorFormSchema, InternalConnectorForm } from '@kbn/alerts-ui-shared'; + +export const serializer = (data: InternalConnectorForm): ConnectorFormSchema => { + const formAllowedChannels = (data.config?.allowedChannels as string[]) ?? []; + const allowedChannels = formAllowedChannels.map((option) => ({ name: option })) ?? []; + + return { + ...data, + config: { ...data.config, allowedChannels }, + }; +}; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.test.tsx index b2c1d36f1c3c0..9eaed9c470a9c 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.test.tsx @@ -24,6 +24,7 @@ const testBlock = { }, ], }; + beforeAll(async () => { const connectorTypeRegistry = new TypeRegistry(); ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock }); @@ -35,14 +36,14 @@ beforeAll(async () => { }); describe('connectorTypeRegistry.get works', () => { - test('connector type static data is as expected', () => { + it('connector type static data is as expected', () => { expect(connectorTypeModel.id).toEqual(CONNECTOR_ID); expect(connectorTypeModel.iconClass).toEqual('logoSlack'); }); }); describe('hideInUi', () => { - test('should return true when slack is enabled in config', () => { + it('should return true when slack is enabled in config', () => { expect( // @ts-expect-error connectorTypeModel.getHideInUi([ @@ -52,7 +53,7 @@ describe('hideInUi', () => { ).toEqual(true); }); - test('should return false when slack is disabled in config', () => { + it('should return false when slack is disabled in config', () => { expect( // @ts-expect-error connectorTypeModel.getHideInUi([ @@ -62,7 +63,7 @@ describe('hideInUi', () => { ).toEqual(false); }); - test('should return false when slack is not found in config', () => { + it('should return false when slack is not found in config', () => { expect( // @ts-expect-error connectorTypeModel.getHideInUi([ @@ -75,7 +76,7 @@ describe('hideInUi', () => { describe('Slack action params validation', () => { describe('postMessage', () => { - test('should succeed when action params include valid message and channels list', async () => { + it('should succeed when action params include valid message and channels list', async () => { const actionParams = { subAction: 'postMessage', subActionParams: { channels: ['general'], text: 'some text' }, @@ -89,7 +90,7 @@ describe('Slack action params validation', () => { }); }); - test('should succeed when action params include valid message and channels and channel ids', async () => { + it('should succeed when action params include valid message and channels and channel ids', async () => { const actionParams = { subAction: 'postMessage', subActionParams: { channels: ['general'], channelIds: ['general'], text: 'some text' }, @@ -103,7 +104,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when channels field is missing in action params', async () => { + it('should fail when channels field is missing in action params', async () => { const actionParams = { subAction: 'postMessage', subActionParams: { text: 'some text' }, @@ -117,7 +118,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when field text does not exist', async () => { + it('should fail when field text does not exist', async () => { const actionParams = { subAction: 'postMessage', subActionParams: { channels: ['general'] }, @@ -131,7 +132,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when text is empty string', async () => { + it('should fail when text is empty string', async () => { const actionParams = { subAction: 'postMessage', subActionParams: { channels: ['general'], text: '' }, @@ -147,7 +148,7 @@ describe('Slack action params validation', () => { }); describe('postBlockkit', () => { - test('should succeed when action params include valid JSON message and channels list', async () => { + it('should succeed when action params include valid JSON message and channels list', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { channels: ['general'], text: JSON.stringify(testBlock) }, @@ -161,7 +162,7 @@ describe('Slack action params validation', () => { }); }); - test('should succeed when action params include valid JSON message and channels and channel ids', async () => { + it('should succeed when action params include valid JSON message and channels and channel ids', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { @@ -179,7 +180,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when channels field is missing in action params', async () => { + it('should fail when channels field is missing in action params', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { text: JSON.stringify(testBlock) }, @@ -193,7 +194,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when field text does not exist', async () => { + it('should fail when field text does not exist', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { channels: ['general'] }, @@ -207,7 +208,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when text is empty string', async () => { + it('should fail when text is empty string', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { channels: ['general'], text: '' }, @@ -221,7 +222,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when text is invalid JSON', async () => { + it('should fail when text is invalid JSON', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { channels: ['general'], text: 'abcd' }, @@ -235,7 +236,7 @@ describe('Slack action params validation', () => { }); }); - test('should fail when text is JSON but does not contain "blocks" property', async () => { + it('should fail when text is JSON but does not contain "blocks" property', async () => { const actionParams = { subAction: 'postBlockkit', subActionParams: { channels: ['general'], text: JSON.stringify({ foo: 'bar' }) }, @@ -249,4 +250,119 @@ describe('Slack action params validation', () => { }); }); }); + + describe('channels', () => { + it('should fail when all channel variables are undefined', async () => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text' }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: ['Channel ID is required.'], + }, + }); + }); + + it('should fail when all channel variables are empty', async () => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text', channelNames: [], channels: [], channelIds: [] }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: ['Channel ID is required.'], + }, + }); + }); + + it.each([ + ['channelIds', undefined], + ['channels', undefined], + ['channelIds', []], + ['channels', []], + ])('should not fail when channelNames is set and %s is %s', async (key, value) => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text', channelNames: ['#test'], [key]: value }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: [], + }, + }); + }); + + it.each([ + ['channelNames', undefined], + ['channels', undefined], + ['channelNames', []], + ['channels', []], + ])('should not fail when channelIds is set and %s is %s', async (key, value) => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text', channelIds: ['channel-id'], [key]: value }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: [], + }, + }); + }); + + it.each([ + ['channelIds', undefined], + ['channelNames', undefined], + ['channelIds', []], + ['channelNames', []], + ])('should not fail when channels is set and %s is %s', async (key, value) => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text', channels: ['my-channel'], [key]: value }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: [], + }, + }); + }); + + it('should not fail if channel names do not start with #', async () => { + const actionParams = { + subAction: 'postMessage', + subActionParams: { text: 'some text', channelNames: ['test'] }, + }; + + expect(await connectorTypeModel.validateParams(actionParams, null)).toEqual({ + errors: { + text: [], + channels: ['Channel name must start with a #'], + }, + }); + }); + }); + + describe('default values', () => { + it('sets the default values as expected', () => { + expect(connectorTypeModel.defaultActionParams).toEqual({ + subAction: 'postMessage', + subActionParams: { text: undefined }, + }); + + expect(connectorTypeModel.defaultRecoveredActionParams).toEqual({ + subAction: 'postMessage', + subActionParams: { text: undefined }, + }); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.tsx index b203f5070d655..72cd9a6184dd4 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_api.tsx @@ -26,19 +26,47 @@ import { SELECT_MESSAGE, JSON_REQUIRED, BLOCKS_REQUIRED, + CHANNEL_NAME_ERROR, } from './translations'; import type { SlackActionParams } from '../types'; import { subtype } from '../slack/slack'; +import { serializer } from './form_serializer'; +import { deserializer } from './form_deserializer'; + +const DEFAULT_PARAMS = { subAction: 'postMessage' as const, subActionParams: { text: undefined } }; + +const getChannelErrors = (channels?: string[], channelIds?: string[], channelNames?: string[]) => { + const isUndefinedOrEmptyArray = (arr?: string[]) => { + if (!arr || arr.length === 0) { + return true; + } + + return false; + }; + + const isChannelNamesEmpty = isUndefinedOrEmptyArray(channelNames); + const isChannelIdsEmpty = isUndefinedOrEmptyArray(channelIds); + const isChannelsEmpty = isUndefinedOrEmptyArray(channels); + + const allEmpty = isChannelNamesEmpty && isChannelIdsEmpty && isChannelsEmpty; + + if (allEmpty) { + return [CHANNEL_REQUIRED]; + } -const isChannelValid = (channels?: string[], channelIds?: string[]) => { if ( - (channels === undefined && !channelIds?.length) || - (channelIds === undefined && !channels?.length) || - (!channelIds?.length && !channels?.length) + channelNames && + channelNames.length > 0 && + channelNames.some((channelName) => !channelName.startsWith('#')) ) { - return false; + return [CHANNEL_NAME_ERROR]; } - return true; + + if (!allEmpty) { + return []; + } + + return []; }; export const getConnectorType = (): ConnectorTypeModel< @@ -63,18 +91,22 @@ export const getConnectorType = (): ConnectorTypeModel< text: new Array(), channels: new Array(), }; + const validationResult = { errors }; + if (actionParams.subAction === 'postMessage' || actionParams.subAction === 'postBlockkit') { if (!actionParams.subActionParams.text) { errors.text.push(MESSAGE_REQUIRED); } - if ( - !isChannelValid( - actionParams.subActionParams.channels, - actionParams.subActionParams.channelIds - ) - ) { - errors.channels.push(CHANNEL_REQUIRED); + + const channelErrors = getChannelErrors( + actionParams.subActionParams.channels, + actionParams.subActionParams.channelIds, + actionParams.subActionParams.channelNames + ); + + if (channelErrors.length > 0) { + errors.channels.push(...channelErrors); } if (actionParams.subAction === 'postBlockkit' && actionParams.subActionParams.text) { @@ -88,6 +120,7 @@ export const getConnectorType = (): ConnectorTypeModel< } } } + return validationResult; }, actionConnectorFields: lazy(() => import('./slack_connectors')), @@ -108,4 +141,10 @@ export const getConnectorType = (): ConnectorTypeModel< } return {}; }, + connectorForm: { + serializer, + deserializer, + }, + defaultActionParams: DEFAULT_PARAMS, + defaultRecoveredActionParams: DEFAULT_PARAMS, }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx index 95679d5795dcb..709c4126435d3 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.test.tsx @@ -6,150 +6,253 @@ */ import React from 'react'; -import { act, render, screen, waitFor } from '@testing-library/react'; -import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { render, screen, waitFor } from '@testing-library/react'; import { createStartServicesMock } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react.mock'; -import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils'; +import { ConnectorFormTestProvider } from '../lib/test_utils'; import { SlackActionFieldsComponents as SlackActionFields } from './slack_connectors'; -import { useValidChannels } from './use_valid_channels'; +import userEvent from '@testing-library/user-event'; +import { serializer } from './form_serializer'; +import { deserializer } from './form_deserializer'; const mockUseKibanaReturnValue = createStartServicesMock(); + jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana', () => ({ __esModule: true, useKibana: jest.fn(() => ({ services: mockUseKibanaReturnValue, })), })); -jest.mock('./use_valid_channels'); - -(useKibana as jest.Mock).mockImplementation(() => ({ - services: { - docLinks: { - links: { - alerting: { slackApiAction: 'url' }, - }, - }, - notifications: { - toasts: { - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }, - }, -})); - -const useValidChannelsMock = useValidChannels as jest.Mock; describe('SlackActionFields renders', () => { const onSubmit = jest.fn(); + beforeEach(() => { - useValidChannelsMock.mockClear(); - onSubmit.mockClear(); jest.clearAllMocks(); - useValidChannelsMock.mockImplementation(() => ({ - channels: [], - isLoading: false, - resetChannelsToValidate: jest.fn(), - })); }); - it('all connector fields is rendered for web_api type', async () => { + describe('creating new connector', () => { const actionConnector = { - secrets: { - token: 'some token', - }, + secrets: {}, config: {}, id: 'test', - actionTypeId: '.slack', + actionTypeId: '.slack_api', name: 'slack', isDeprecated: false, }; - render( - - {}} /> - - ); + it('all connector fields is rendered for web_api type', async () => { + render( + + {}} + /> + + ); - expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); - expect(screen.getByTestId('secrets.token-input')).toHaveValue('some token'); - }); + expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); + expect(screen.getByTestId('config.allowedChannels-input')).toBeInTheDocument(); + }); - it('connector validation succeeds when connector config is valid for Web API type', async () => { - const actionConnector = { - secrets: { - token: 'some token', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - isDeprecated: false, - }; + it('creates a new connector correctly', async () => { + render( + + {}} + /> + + ); - // Simulate that user just type a channel ID - useValidChannelsMock.mockImplementation(() => ({ - channels: ['my-channel'], - isLoading: false, - resetChannelsToValidate: jest.fn(), - })); - - render( - - {}} /> - - ); - await waitForComponentToUpdate(); - act(() => { - screen.getByTestId('form-test-provide-submit').click(); - }); + expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); + expect(screen.getByTestId('config.allowedChannels-input')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('secrets.token-input')); + await userEvent.paste('some token'); + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#general{enter}'); - await waitFor(() => { - expect(onSubmit).toBeCalledTimes(1); - expect(onSubmit).toBeCalledWith({ - data: { - secrets: { - token: 'some token', + await userEvent.click(screen.getByTestId('form-test-provide-submit')); + + await waitFor(() => { + expect(onSubmit).toBeCalledTimes(1); + expect(onSubmit).toBeCalledWith({ + data: { + ...actionConnector, + secrets: { + token: 'some token', + }, + config: { + allowedChannels: [{ name: '#general' }], + }, }, - config: { - allowedChannels: ['my-channel'], + isValid: true, + }); + }); + }); + + it('can create a connector without allowed channels', async () => { + render( + + {}} + /> + + ); + + expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); + expect(screen.getByTestId('config.allowedChannels-input')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('secrets.token-input')); + await userEvent.paste('some token'); + + await userEvent.click(screen.getByTestId('form-test-provide-submit')); + + await waitFor(() => { + expect(onSubmit).toBeCalledTimes(1); + expect(onSubmit).toBeCalledWith({ + data: { + ...actionConnector, + secrets: { + token: 'some token', + }, + config: { + allowedChannels: [], + }, }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - isDeprecated: false, - }, - isValid: true, + isValid: true, + }); + }); + }); + + it('validates allowed channels correctly', async () => { + render( + + {}} + /> + + ); + + expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument(); + expect(screen.getByTestId('config.allowedChannels-input')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('secrets.token-input')); + await userEvent.paste('some token'); + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), 'general{enter}'); + + await userEvent.click(screen.getByTestId('form-test-provide-submit')); + + await waitFor(() => { + expect(onSubmit).toBeCalledTimes(1); + expect(onSubmit).toBeCalledWith(expect.objectContaining({ isValid: false })); }); }); }); - it('connector validation should failed when allowedChannels is empty', async () => { + describe('editing connector', () => { const actionConnector = { secrets: { token: 'some token', }, + config: { allowedChannels: [{ id: 'channel-id', name: '#test' }] }, id: 'test', - actionTypeId: '.slack', + actionTypeId: '.slack_api', name: 'slack', - config: {}, isDeprecated: false, }; - render( - - {}} /> - - ); - await waitForComponentToUpdate(); - act(() => { - screen.getByTestId('form-test-provide-submit').click(); + it('renders correctly the values of the connector', async () => { + render( + + {}} + /> + + ); + + expect(screen.getByTestId('secrets.token-input')).toHaveValue('some token'); + expect(screen.getByText('#test')).toBeInTheDocument(); }); - await waitFor(() => { - expect(onSubmit).toBeCalledTimes(1); - expect(onSubmit).toBeCalledWith(expect.objectContaining({ isValid: false })); + it('changes the values correctly', async () => { + render( + + {}} + /> + + ); + + await userEvent.clear(screen.getByTestId('secrets.token-input')); + await userEvent.click(screen.getByTestId('secrets.token-input')); + await userEvent.paste('token updated'); + + await userEvent.click(screen.getByTestId('comboBoxClearButton')); + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#new-channel{enter}'); + + await userEvent.click(screen.getByTestId('form-test-provide-submit')); + + await waitFor(() => { + expect(onSubmit).toBeCalledTimes(1); + expect(onSubmit).toBeCalledWith({ + data: { + ...actionConnector, + secrets: { + token: 'token updated', + }, + config: { + allowedChannels: [{ name: '#new-channel' }], + }, + }, + isValid: true, + }); + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx index 4223f7bba881d..87018e0d23680 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_connectors.tsx @@ -5,26 +5,18 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React from 'react'; import type { ActionConnectorFieldsProps, ConfigFieldSchema, SecretsFieldSchema, } from '@kbn/triggers-actions-ui-plugin/public'; import { SimpleConnectorForm, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocLinksStart } from '@kbn/core/public'; -import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { debounce, isEmpty, isEqual } from 'lodash'; -import { QueryClient, QueryClientProvider } from '@kbn/react-query'; import * as i18n from './translations'; -import { useValidChannels } from './use_valid_channels'; - -/** wait this many ms after the user completes typing before applying the filter input */ -const INPUT_TIMEOUT = 250; const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] => [ { @@ -42,152 +34,65 @@ const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] => }, ]; -interface AllowedChannels { - id: string; - name: string; -} - -const getConfigFormSchemaAfterSecrets = ({ - options, - isLoading, - isDisabled, - onChange, - onCreateOption, - selectedOptions, -}: { - options: Array>; - isLoading: boolean; - isDisabled: boolean; - onChange: (options: Array>) => void; - onCreateOption: (searchValue: string, options: EuiComboBoxOptionOption[]) => void; - selectedOptions: Array>; -}): ConfigFieldSchema[] => [ +const getConfigFormSchema = (): ConfigFieldSchema[] => [ { id: 'allowedChannels', - isRequired: true, + isRequired: false, label: i18n.ALLOWED_CHANNELS, type: 'COMBO_BOX', + validations: [ + { + isBlocking: true, + validator: (args) => { + const valueAsArray = Array.isArray(args.value) ? args.value : [args.value]; + + if (valueAsArray.length === 0) { + return; + } + + const areAllValid = valueAsArray.every((value) => value.startsWith('#')); + + if (areAllValid) { + return; + } + + return { + code: 'ERR_FIELD_FORMAT', + formatType: 'COMBO_BOX', + message: i18n.CHANNEL_NAME_ERROR, + }; + }, + }, + ], euiFieldProps: { noSuggestions: true, - isDisabled, - isLoading, - options, - onChange, - onCreateOption, - selectedOptions, + autoComplete: 'off', + append: ( + + {i18n.OPTIONAL_LABEL} + + ), }, }, ]; -const NO_SCHEMA: ConfigFieldSchema[] = []; -const SEPARATOR = ' - '; export const SlackActionFieldsComponents: React.FC = ({ readOnly, isEdit, }) => { const { docLinks } = useKibana().services; - const form = useFormContext(); - const { setFieldValue } = form; - const [formData] = useFormData({ form }); - const [authToken, setAuthToken] = useState(''); - const [channelsToValidate, setChannelsToValidate] = useState(''); - const { - channels: validChannels, - isLoading, - resetChannelsToValidate, - } = useValidChannels({ - authToken, - channelId: channelsToValidate, - }); - - const onCreateOption = useCallback((searchValue: string, options: EuiComboBoxOptionOption[]) => { - setChannelsToValidate(searchValue); - }, []); - const onChange = useCallback( - (options: Array>) => { - const tempChannelIds: AllowedChannels[] = options.map( - (opt: EuiComboBoxOptionOption) => { - return opt.value!; - } - ); - setChannelsToValidate(''); - resetChannelsToValidate(tempChannelIds); - }, - [resetChannelsToValidate] - ); - - const configFormSchemaAfterSecrets = useMemo(() => { - const validChannelsFormatted = validChannels.map((channel) => ({ - label: `${channel.id}${SEPARATOR}${channel.name}`, - value: channel, - })); - return getConfigFormSchemaAfterSecrets({ - options: validChannelsFormatted, - isLoading, - isDisabled: (authToken || '').length === 0, - onChange, - onCreateOption, - selectedOptions: validChannelsFormatted, - }); - }, [validChannels, isLoading, authToken, onChange, onCreateOption]); - - const debounceSetToken = debounce(setAuthToken, INPUT_TIMEOUT); - useEffect(() => { - if (formData.secrets && formData.secrets.token !== authToken) { - debounceSetToken(formData.secrets.token); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [formData.secrets]); - - useEffect(() => { - if (isEmpty(authToken) && validChannels.length > 0) { - setFieldValue('config.allowedChannels', []); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [authToken]); - - useEffect(() => { - const configAllowedChannels = formData?.config?.allowedChannels || []; - if (!isEqual(configAllowedChannels, validChannels)) { - setFieldValue('config.allowedChannels', validChannels); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [validChannels]); - - const isInitialyzed = useRef(false); - useEffect(() => { - const configAllowedChannels = formData?.config?.allowedChannels || []; - if ( - !isInitialyzed.current && - configAllowedChannels.length > 0 && - !isEqual(configAllowedChannels, validChannels) - ) { - isInitialyzed.current = true; - resetChannelsToValidate(formData.config.allowedChannels); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [formData.config]); - return ( ); }; -export const simpleConnectorQueryClient = new QueryClient(); - -const SlackActionFields: React.FC = (props) => ( - - - -); +const SlackActionFields = React.memo(SlackActionFieldsComponents); // eslint-disable-next-line import/no-default-export export { SlackActionFields as default }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.test.tsx index 661f09d6dbd7a..97f395c70f7a5 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.test.tsx @@ -6,53 +6,20 @@ */ import React from 'react'; -import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import SlackParamsFields from './slack_params'; -import type { UseSubActionParams } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_sub_action'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; +import type { AppMockRenderer } from '../lib/test_utils'; +import { createAppMockRenderer } from '../lib/test_utils'; import userEvent from '@testing-library/user-event'; -interface Result { - isLoading: boolean; - response: Record; - error: null | Error; -} - const triggersActionsPath = '@kbn/triggers-actions-ui-plugin/public'; -const mockUseValidChanelId = jest.fn().mockImplementation(() => ({ - isLoading: false, - response: { - channel: { - id: 'id', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, - }, - }, - error: null, -})); -const testBlock = { - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: "Hello, Assistant to the Regional Manager Dwight! *Michael Scott* wants to know where you'd like to take the Paper Company investors to dinner tonight.\n", - }, - }, - ], -}; -const mockUseSubAction = jest.fn]>(mockUseValidChanelId); - const mockToasts = { addDanger: jest.fn(), addWarning: jest.fn() }; + jest.mock(triggersActionsPath, () => { const original = jest.requireActual(triggersActionsPath); return { ...original, - useSubAction: (params: UseSubActionParams) => mockUseSubAction(params), useKibana: () => ({ ...original.useKibana(), services: { @@ -62,439 +29,520 @@ jest.mock(triggersActionsPath, () => { }; }); -describe('SlackParamsFields renders', () => { - beforeEach(() => { - mockUseSubAction.mockClear(); - mockUseValidChanelId.mockClear(); - mockUseValidChanelId.mockImplementation(() => ({ - isLoading: false, - response: { - channel: { - id: 'id', - name: 'general', - is_channel: true, - is_archived: false, - is_private: true, +describe('SlackParamsFields', () => { + const actionConnector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.slack_api', + name: 'slack', + isDeprecated: false, + isPreconfigured: false as const, + isSystemAction: false, + isConnectorTypeDeprecated: false, + }; + + const actionConnectorWithAllowedList = { + ...actionConnector, + id: 'test-2', + config: { allowedChannels: [{ name: '#test' }, { name: '#general' }] }, + }; + + const actionParams = { + subAction: 'postMessage' as const, + subActionParams: { + channels: ['my-channel'], + channelIds: ['my-channel-id'], + channelNames: ['my-channel-name'], + text: 'a new slack message', + }, + }; + + const testBlock = JSON.stringify({ + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Some **text**', }, }, - error: null, - })); + ], }); - test('when useDefaultMessage is set to true and the default message changes, the underlying message is replaced with the default message', () => { - const editAction = jest.fn(); - const { rerender } = render( - - - + + const editAction = jest.fn(); + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('changing connector resets the fields', async () => { + const { rerender } = appMockRenderer.render( + ); + expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('some text'); + + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); + rerender( - - - - ); - expect(editAction).toHaveBeenCalledWith( - 'subActionParams', - { channels: ['general'], channelIds: [], text: 'some different default message' }, - 0 + ); + + expect(screen.getByTestId('webApiTextTextArea')).toHaveValue(''); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: undefined }, + 0 + ); + }); }); - test('when useDefaultMessage is set to false and the default message changes, the underlying message is not changed, Web API', () => { - const editAction = jest.fn(); - const { rerender } = render( - - - + it('changing the index resets the fields', async () => { + const { rerender } = appMockRenderer.render( + ); + expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('some text'); + + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); rerender( - - - + ); - expect(editAction).not.toHaveBeenCalled(); + + expect(screen.getByTestId('webApiTextTextArea')).toHaveValue(''); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: undefined }, + 0 + ); + }); }); - test('default to text field when no existing subaction params', async () => { - render( - - {}} - index={0} - defaultMessage="default message" - messageVariables={[]} - /> - + it('changing the connector resets the fields to the new params', async () => { + const { rerender } = appMockRenderer.render( + ); expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextTextArea')).toHaveValue(''); - }); - test('correctly renders params fields for postMessage subaction', async () => { - render( - - {}} - index={0} - defaultMessage="default message" - messageVariables={[]} - /> - + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); + + rerender( + ); - expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); - expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('some text'); + expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('a new slack message'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subActionParams', actionParams.subActionParams, 0); + }); }); - test('correctly renders params fields for postBlockkit subaction', async () => { - render( - - {}} - index={0} - defaultMessage="default message" - messageVariables={[]} - /> - + it('uses the default value if the text is not defined', async () => { + appMockRenderer.render( + ); - expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); - expect(screen.getByTestId('webApiBlock')).toBeInTheDocument(); + expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('my default message'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: 'my default message' }, + 0 + ); + }); }); - test('should toggle subaction when button group clicked', async () => { - const mockEditFunc = jest.fn(); - const { getByTestId } = render( - - - + it('does not uses the default value if text is defined', async () => { + appMockRenderer.render( + ); - getByTestId('blockkit').click(); - expect(mockEditFunc).toBeCalledWith('subAction', 'postBlockkit', 0); + expect(screen.getByTestId('webApiTextTextArea')).toHaveValue('a new slack message'); - getByTestId('text').click(); - expect(mockEditFunc).toBeCalledWith('subAction', 'postMessage', 0); + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subActionParams', actionParams.subActionParams, 0); + }); }); - test('show the Channel label when using the old attribute "channels" in subActionParams', async () => { - const mockEditFunc = jest.fn(); - const WrappedComponent = () => { - return ( - - - - ); - }; - const { getByTestId } = render(); + it('sets the inital values on first render correctly', async () => { + appMockRenderer.render( + + ); - expect(screen.findByText('Channel')).toBeTruthy(); - expect(screen.getByTestId('slackApiChannelId')).toBeInTheDocument(); - expect(getByTestId('slackApiChannelId')).toHaveValue('old channel name'); + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subAction', 'postMessage', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: undefined }, + 0 + ); + }); }); - test('show the Channel ID label when using the new attribute "channelIds" in subActionParams', async () => { - const mockEditFunc = jest.fn(); - const WrappedComponent: React.FunctionComponent = () => { - return ( - - - + describe('new params', () => { + const params = {}; + + it('renders the initial fields correctly', () => { + appMockRenderer.render( + ); - }; - const { getByTestId } = render(); - expect(screen.findByText('Channel ID')).toBeTruthy(); - expect(screen.getByTestId('slackApiChannelId')).toBeInTheDocument(); - expect(getByTestId('slackApiChannelId')).toHaveValue('channel-id-xxx'); - }); + expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); + expect(screen.getByTestId('slackChannelsComboBox')).toBeInTheDocument(); + expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); + }); - test('channel id in subActionParams should be validated', async () => { - const mockEditFunc = jest.fn(); - mockUseValidChanelId.mockImplementation(() => ({ - isLoading: false, - response: { - channel: { - id: 'new-channel-id', - name: 'new channel id', - is_channel: true, - is_archived: false, - is_private: true, - }, - }, - error: null, - })); - const WrappedComponent = () => { - return ( - - - + it('changes the message type correctly', async () => { + appMockRenderer.render( + ); - }; - const { getByTestId } = render(); - getByTestId('slackApiChannelId').click(); - await userEvent.clear(getByTestId('slackApiChannelId')); - fireEvent.change(getByTestId('slackApiChannelId'), { - target: { value: 'new-channel-id' }, + expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Block Kit')); + + expect(await screen.findByTestId('webApiBlock')).toBeInTheDocument(); }); - await userEvent.tab(); - await waitFor(() => { - expect(mockEditFunc).toBeCalledWith( - 'subActionParams', - { channelIds: ['new-channel-id'], channels: undefined, text: 'some text' }, - 0 + it('fill out all params (text) and submits correctly', async () => { + appMockRenderer.render( + ); - expect(mockUseSubAction).toBeCalledWith({ - connectorId: 'connector-id', - disabled: false, - subAction: 'validChannelId', - subActionParams: { - channelId: 'new-channel-id', - }, + + expect(screen.getByTestId('webApiTextTextArea')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subAction', 'postMessage', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: 'a slack message' }, + 0 + ); }); }); - }); - test('channel id work with combobox when allowedChannels pass in the config attributes', async () => { - const mockEditFunc = jest.fn(); - const WrappedComponent = () => { - return ( - - - + it('fill out all params (blockKit) and submits correctly', async () => { + appMockRenderer.render( + ); - }; - const { getByTestId } = render(); - expect(screen.findByText('Channel ID')).toBeTruthy(); - expect(getByTestId('slackChannelsComboBox')).toBeInTheDocument(); - expect(getByTestId('slackChannelsComboBox').textContent).toBe('channel-id-1 - channel 1'); + expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); + await userEvent.click(screen.getByText('Block Kit')); + expect(await screen.findByTestId('webApiBlock')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('webApiBlockJsonEditor')); + await userEvent.paste(testBlock); - act(() => { - const combobox = getByTestId('slackChannelsComboBox'); - const inputCombobox = within(combobox).getByTestId('comboBoxSearchInput'); - inputCombobox.click(); + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subAction', 'postBlockkit', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { channels: [], channelIds: [], channelNames: [], text: testBlock }, + 0 + ); + }); }); - await waitFor(() => { - // const popOverElement = within(baseElement).getByTestId('slackChannelsComboBox-optionsList'); - expect(screen.getByTestId('channel-id-1')).toBeInTheDocument(); - expect(screen.getByTestId('channel-id-2')).toBeInTheDocument(); - expect(screen.getByTestId('channel-id-3')).toBeInTheDocument(); + it('resets the textarea when changing tabs', async () => { + appMockRenderer.render( + + ); + + expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); + + await userEvent.click(screen.getByText('Block Kit')); + expect(await screen.findByTestId('webApiBlock')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Text')); + expect(await screen.findByTestId('webApiTextTextArea')).toHaveValue(''); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subAction', 'postMessage', 0); + }); }); - act(() => { - screen.getByTestId('channel-id-3').click(); + it('changes the subAction correctly when changing tabs', async () => { + appMockRenderer.render( + + ); + + expect(screen.getByTestId('slackMessageTypeChangeButton')).toBeInTheDocument(); + await userEvent.click(screen.getByText('Block Kit')); + expect(await screen.findByTestId('webApiBlock')).toBeInTheDocument(); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith('subAction', 'postBlockkit', 0); + }); }); - await waitFor(() => { - expect( - within(getByTestId('slackChannelsComboBox')).getByText('channel-id-3 - channel 3') - ).toBeInTheDocument(); - expect(mockEditFunc).toBeCalledWith( - 'subActionParams', - { channelIds: ['channel-id-3'], channels: undefined, text: 'some text' }, - 0 + it('can set any channel when the allowlist is empty', async () => { + appMockRenderer.render( + ); - expect(mockUseSubAction).toBeCalledWith({ - connectorId: 'connector-id', - disabled: false, - subAction: 'validChannelId', - subActionParams: { channelId: '' }, + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#my-channel{enter}'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + channels: undefined, + channelIds: undefined, + channelNames: ['#my-channel'], + text: undefined, + }, + 0 + ); }); }); - }); - test('show error message when no channel is selected', async () => { - render( - + it('cannot set channels not in the allow list', async () => { + appMockRenderer.render( {}} + actionParams={params} + editAction={editAction} index={0} - defaultMessage="default message" - messageVariables={[]} + errors={{ message: [] }} + actionConnector={actionConnectorWithAllowedList} /> - + ); + + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#my-channel{enter}'); + + expect(await screen.findByText(`doesn't match any options`)).toBeInTheDocument(); + }); + }); + + describe('editing params', () => { + const actionParamsWithChannels = { + subAction: 'postMessage', + subActionParams: { channels: ['my-channel'] }, + }; + + const actionParamsWithChannelsIds = { + subAction: 'postMessage', + subActionParams: { channelIds: ['my-channel-id'] }, + }; + + const actionParamsWithChannelNames = { + subAction: 'postMessage', + subActionParams: { channelNames: ['my-channel-name'] }, + }; + + const tests: [string, Record, string][] = [ + ['channels', actionParamsWithChannels, 'my-channel'], + ['channelIds', actionParamsWithChannelsIds, 'my-channel-id'], + ['channelNames', actionParamsWithChannelNames, 'my-channel-name'], + ]; + + it.each(tests)( + 'renders the initial fields correctly %s', + async (_, params, expectedChannel) => { + appMockRenderer.render( + + ); + expect(await screen.findByText(expectedChannel)).toBeInTheDocument(); + } + ); + + it.each(tests)( + 'changes to channelNames when editing the channels for %s', + async (_, params, expectedChannel) => { + appMockRenderer.render( + + ); + + expect(await screen.findByText(expectedChannel)).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('comboBoxClearButton')); + await userEvent.click(screen.getByTestId('comboBoxSearchInput')); + await userEvent.type(screen.getByTestId('comboBoxSearchInput'), '#my-channel{enter}'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + channels: undefined, + channelIds: undefined, + channelNames: ['#my-channel'], + text: undefined, + }, + 0 + ); + }); + } + ); + + it.each(tests)( + 'does not change the %s when editing the text field', + async (key, params, expectedChannel) => { + appMockRenderer.render( + + ); + + expect(await screen.findByText(expectedChannel)).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('webApiTextTextArea')); + await userEvent.paste('a slack message'); + + await waitFor(() => { + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + channels: [], + channelIds: [], + channelNames: [], + /** + * The value below will overwrite one of the values above. + * For each test a different value will be overwritten. + */ + [key]: [expectedChannel], + text: 'a slack message', + }, + 0 + ); + }); + } ); - expect(screen.getByText('my error message')).toBeInTheDocument(); }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.tsx index adb9d93b41894..9d13fa791ac15 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/slack_params.tsx @@ -16,23 +16,57 @@ import { EuiSpacer, EuiFormRow, EuiComboBox, - EuiFieldText, EuiButtonGroup, EuiLink, + useGeneratedHtmlId, } from '@elastic/eui'; -import { useSubAction, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; import type { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; import type { PostBlockkitParams, PostMessageParams, SlackApiConfig, - ValidChannelIdSubActionParams, } from '@kbn/connector-schemas/slack_api'; -import type { ValidChannelResponse } from '../../../common/slack_api/types'; -const SlackParamsFields: React.FunctionComponent< - ActionParamsProps -> = ({ +type ParamsProps = ActionParamsProps; + +const SlackParamsFields: React.FunctionComponent = (props) => { + const { editAction, index, useDefaultMessage, defaultMessage } = props; + const { subActionParams } = props.actionParams; + const { channels = [], channelIds = [], channelNames = [], text } = subActionParams ?? {}; + + const connectorId = props.actionConnector?.id ?? ''; + const key = `${connectorId}:${index}`; + + useEffect(() => { + editAction('subAction', 'postMessage', index); + editAction( + 'subActionParams', + { + channels, + channelIds, + channelNames, + text, + }, + index + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectorId, index]); + + useEffect(() => { + if (shouldUseDefaultValue({ useDefaultMessage, defaultMessage, text })) { + editAction( + 'subActionParams', + { channels, channelIds, channelNames, text: defaultMessage }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultMessage, useDefaultMessage]); + + return ; +}; + +const SlackParamsFieldsComponent: React.FunctionComponent = ({ actionConnector, actionParams, editAction, @@ -42,254 +76,136 @@ const SlackParamsFields: React.FunctionComponent< defaultMessage, useDefaultMessage, }) => { - const [connectorId, setConnectorId] = useState(); - const { subAction, subActionParams } = actionParams; - const { channels = [], text, channelIds = [] } = subActionParams ?? {}; - const [tempChannelId, setTempChannelId] = useState( - channels.length > 0 - ? channels[0] - : channelIds.length > 0 && channelIds[0].length > 0 - ? channelIds[0] - : '' - ); + const channelsLabelId = useGeneratedHtmlId(); + + const { subActionParams } = actionParams; + const { channels = [], text, channelIds = [], channelNames = [] } = subActionParams ?? {}; + + const selectedChannel = getSelectedChannel({ + channels, + channelIds, + channelNames, + }); + + const initialTextValue = shouldUseDefaultValue({ useDefaultMessage, defaultMessage, text }) + ? defaultMessage + : text; + const [messageType, setMessageType] = useState('text'); - const [textValue, setTextValue] = useState(text); - const [validChannelId, setValidChannelId] = useState(''); - const { toasts } = useKibana().services.notifications; + const [textValue, setTextValue] = useState(initialTextValue); + const allowedChannelsConfig = (actionConnector as UserConfiguredActionConnector)?.config ?.allowedChannels ?? []; - const [selectedChannels, setSelectedChannels] = useState( - (channelIds ?? []).map((c) => { - const allowedChannelSelected = allowedChannelsConfig?.find((ac) => ac.id === c); - return { - value: c, - label: allowedChannelSelected - ? `${allowedChannelSelected.id} - ${allowedChannelSelected.name}` - : c, - }; - }) + + const slackChannelsOptions = allowedChannelsConfig.map((ac) => ({ + label: formatChannel(ac.name), + value: formatChannel(ac.name), + 'data-test-subj': ac.name, + })); + + const hasAllowedChannelsConfig = Boolean(allowedChannelsConfig.length); + + const [selectedChannels, setSelectedChannels] = useState[]>( + selectedChannel ? [{ value: selectedChannel, label: selectedChannel }] : [] ); - const [channelValidError, setChannelValidError] = useState([]); - const { - response: { channel: channelValidInfo } = {}, - isLoading: isValidatingChannel, - error: channelValidErrorResp, - } = useSubAction({ - connectorId: actionConnector?.id, - subAction: 'validChannelId', - subActionParams: { - channelId: validChannelId, - }, - disabled: validChannelId.length === 0 && allowedChannelsConfig.length === 0, - }); const onToggleInput = useCallback( (id: string) => { // clear the text when toggled setTextValue(''); - editAction('subAction', id === 'text' ? 'postMessage' : 'postBlockkit', index); + editAction('subAction', getSubAction(id), index); setMessageType(id); }, - [setMessageType, editAction, index, setTextValue] + [editAction, index] ); - useEffect(() => { - if (useDefaultMessage || !text) { - editAction('subActionParams', { channels, channelIds, text: defaultMessage }, index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultMessage, useDefaultMessage]); + const onChangeComboBox = useCallback( + (newOptions: EuiComboBoxOptionOption[]) => { + const newSelectedChannels = newOptions.map((option) => option.value!.toString()); + setSelectedChannels(newOptions); - useEffect(() => { - if ( - !isValidatingChannel && - !channelValidErrorResp && - channelValidInfo && - validChannelId === channelValidInfo.id - ) { editAction( 'subActionParams', - { channels: undefined, channelIds: [channelValidInfo.id], text }, + { channels: undefined, channelIds: undefined, channelNames: newSelectedChannels, text }, index ); - setValidChannelId(''); - setChannelValidError([]); - } - }, [ - channelValidInfo, - validChannelId, - channelValidErrorResp, - isValidatingChannel, - editAction, - text, - index, - ]); + }, + [editAction, index, text] + ); - useEffect(() => { - if (channelValidErrorResp && validChannelId.length > 0) { - editAction('subActionParams', { channels: undefined, channelIds: [], text }, index); - const errorMessage = i18n.translate( - 'xpack.stackConnectors.slack.params.componentError.validChannelsRequestFailed', - { - defaultMessage: '{validChannelId} is not a valid Slack channel', - values: { - validChannelId, - }, - } - ); - setChannelValidError([errorMessage]); - setValidChannelId(''); - toasts.addDanger({ - title: errorMessage, - text: channelValidErrorResp.message, - }); - } - }, [toasts, channelValidErrorResp, validChannelId, editAction, text, index]); + const onCreateOption = useCallback( + (searchValue: string) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); - useEffect(() => { - if (subAction) { - setMessageType(subAction === 'postMessage' ? 'text' : 'blockkit'); - } - }, [subAction]); + if (!normalizedSearchValue) { + return; + } - useEffect(() => { - // Reset channel id input when we changes connector - if (connectorId && connectorId !== actionConnector?.id) { - editAction('subActionParams', { channels: undefined, channelIds: [], text }, index); - setTempChannelId(''); - setValidChannelId(''); - setChannelValidError([]); - setSelectedChannels([]); - } - setConnectorId(actionConnector?.id ?? ''); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector?.id]); - - if (!subAction) { - editAction('subAction', 'postMessage', index); - } - if (!subActionParams) { - editAction( - 'subActionParams', - { - channels, - channelIds, - text, - }, - index - ); - } - const typeChannelInput = useMemo(() => { - if (channels.length > 0 && channelIds.length === 0) { - return 'channel-name'; - } else if ( - ( - (actionConnector as UserConfiguredActionConnector)?.config - ?.allowedChannels ?? [] - ).length > 0 - ) { - return 'channel-allowed-ids'; - } - return 'channel-id'; - }, [actionConnector, channelIds.length, channels.length]); + const newOption = { + label: searchValue, + }; - const slackChannelsOptions = useMemo(() => { - return ( - (actionConnector as UserConfiguredActionConnector)?.config - ?.allowedChannels ?? [] - ).map((ac) => ({ - label: `${ac.id} - ${ac.name}`, - value: ac.id, - 'data-test-subj': ac.id, - })); - }, [actionConnector]); + setSelectedChannels([newOption]); - const onChangeComboBox = useCallback( - (newOptions: EuiComboBoxOptionOption[]) => { - const newSelectedChannels = newOptions.map((option) => option.value!.toString()); - setSelectedChannels(newOptions); editAction( 'subActionParams', - { channels: undefined, channelIds: newSelectedChannels, text }, + { channels: undefined, channelIds: undefined, channelNames: [searchValue], text }, index ); }, [editAction, index, text] ); - const onBlurChannelIds = useCallback(() => { - if (tempChannelId === '') { - editAction('subActionParams', { channels: undefined, channelIds: [], text }, index); - } - setValidChannelId(tempChannelId.trim()); - }, [editAction, index, tempChannelId, text]); - const onChangeTextField = useCallback( - (evt: React.ChangeEvent) => { - editAction('subActionParams', { channels: undefined, channelIds: [], text }, index); - setTempChannelId(evt.target.value); - }, - [editAction, index, text] - ); + const onTextChange = (value: string) => { + setTextValue(value); + editAction('subAction', getSubAction(messageType), index); + editAction('subActionParams', { channels, channelIds, channelNames, text: value }, index); + }; const channelInput = useMemo(() => { - if (typeChannelInput === 'channel-name' || typeChannelInput === 'channel-id') { - return ( - 0} - fullWidth={true} - /> - ); - } return ( { + if (!channelNames || channelNames?.length === 0) { + editAction('subActionParams', { channels, channelIds, channelNames: [], text }, index); + } + }} /> ); }, [ - channelValidError.length, - isValidatingChannel, - onBlurChannelIds, + channelIds, + channelNames, + channels, + editAction, + hasAllowedChannelsConfig, + index, onChangeComboBox, - onChangeTextField, + onCreateOption, selectedChannels, slackChannelsOptions, - tempChannelId, - typeChannelInput, + text, ]); return ( <> 0 ? channelValidError : (errors.channels as string[])} - isInvalid={Number(errors.channels?.length) > 0 || channelValidError.length > 0} - helpText={ - channelIds.length > 0 && channelValidInfo - ? `${channelValidInfo.id} - ${channelValidInfo.name}` - : '' - } + error={errors.channels as string[]} + isInvalid={Number(errors.channels?.length) > 0} + id={channelsLabelId} > {channelInput} @@ -321,58 +237,19 @@ const SlackParamsFields: React.FunctionComponent< )} {messageType === 'text' ? ( - { - setTextValue(value); - editAction('subActionParams', { channels, channelIds, text: value }, index); - }} - messageVariables={messageVariables} - paramsProperty="webApiText" - inputTargetValue={textValue} - label={i18n.translate( - 'xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', - { - defaultMessage: 'Message', - } - )} + onChange={onTextChange} + value={textValue} errors={(errors.text ?? []) as string[]} /> ) : ( - <> - { - setTextValue(json); - editAction('subActionParams', { channels, channelIds, text: json }, index); - }} - messageVariables={messageVariables} - paramsProperty="webApiBlock" - inputTargetValue={textValue} - label={i18n.translate( - 'xpack.stackConnectors.components.slack.messageJsonAreaFieldLabel', - { - defaultMessage: 'Block Kit', - } - )} - dataTestSubj="webApiBlock" - errors={(errors.text ?? []) as string[]} - /> - {text && ( - <> - - - - - - )} - + )} ); @@ -380,3 +257,116 @@ const SlackParamsFields: React.FunctionComponent< // eslint-disable-next-line import/no-default-export export { SlackParamsFields as default }; + +type TextMessageProps = Pick & { + errors: string[]; + value?: string; + onChange: (value: string) => void; +}; + +const TextMessage = ({ index, onChange, messageVariables, errors, value }: TextMessageProps) => { + return ( + { + onChange(newValue); + }} + messageVariables={messageVariables} + paramsProperty="webApiText" + inputTargetValue={value} + label={i18n.translate('xpack.stackConnectors.components.slack.messageTextAreaFieldLabel', { + defaultMessage: 'Message', + })} + errors={errors} + /> + ); +}; + +type BlockKitMessageProps = Pick & { + errors: string[]; + textValue?: string; + onTextChange: (value: string) => void; +}; + +const BlockKitMessage = ({ + onTextChange, + messageVariables, + errors, + textValue, +}: BlockKitMessageProps) => { + return ( + <> + + {textValue && ( + <> + + + + + + )} + + ); +}; + +const getSelectedChannel = ({ + channels, + channelIds, + channelNames, +}: { + channels?: string[]; + channelIds?: string[]; + channelNames?: string[]; +}) => { + if (channelNames && channelNames.length > 0) { + return channelNames[0]; + } + + if (channelIds && channelIds.length > 0) { + return channelIds[0]; + } + + if (channels && channels.length > 0) { + return channels[0]; + } + + return null; +}; + +const formatChannel = (channel: string): string => { + if (channel.startsWith('#')) { + return channel; + } + + return `#${channel}`; +}; + +const getSubAction = (messageType: string) => + messageType === 'text' ? 'postMessage' : 'postBlockkit'; + +const shouldUseDefaultValue = ({ + useDefaultMessage, + defaultMessage, + text, +}: { + useDefaultMessage?: boolean; + defaultMessage?: string; + text?: string; +}) => (Boolean(useDefaultMessage) || Boolean(defaultMessage)) && !Boolean(text); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/translations.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/translations.ts index f08864bda94b9..6858f73f0c6d8 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/translations.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/translations.ts @@ -43,43 +43,34 @@ export const ACTION_TYPE_TITLE = i18n.translate( export const ALLOWED_CHANNELS = i18n.translate( 'xpack.stackConnectors.components.slack_api.allowedChannelsLabel', { - defaultMessage: 'Channel IDs', + defaultMessage: 'Allowed channel names', } ); -export const SUCCESS_FETCH_CHANNELS = i18n.translate( - 'xpack.stackConnectors.components.slack_api.successFetchChannelsText', + +export const JSON_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired', { - defaultMessage: 'Fetch all channels', + defaultMessage: 'Block kit must be valid JSON.', } ); -export const ERROR_VALID_CHANNELS = i18n.translate( - 'xpack.stackConnectors.components.slack_api.errorValidChannelsText', +export const BLOCKS_REQUIRED = i18n.translate( + 'xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired', { - defaultMessage: - 'Cannot valid channels, please check the validity of your token or your channel', + defaultMessage: `JSON must contain field "blocks".`, } ); -export const ERROR_INVALID_CHANNELS = (invalidChannels: string[]) => - i18n.translate('xpack.stackConnectors.components.slack_api.errorInvalidChannelsText', { - defaultMessage: - 'Cannot validate channel ID "{channels}", please check the validity of your token and/or the channel ID', - values: { - channels: invalidChannels.join(', '), - }, - }); - -export const JSON_REQUIRED = i18n.translate( - 'xpack.stackConnectors.components.slack_api.error.slackBlockkitJsonRequired', +export const OPTIONAL_LABEL = i18n.translate( + 'xpack.stackConnectors.components.slack_api.optionalLabel', { - defaultMessage: 'Block kit must be valid JSON.', + defaultMessage: 'Optional', } ); -export const BLOCKS_REQUIRED = i18n.translate( - 'xpack.stackConnectors.components.slack_api.error.slackBlockkitBlockRequired', +export const CHANNEL_NAME_ERROR = i18n.translate( + 'xpack.stackConnectors.components.slack_api.channelNameError', { - defaultMessage: `JSON must contain field "blocks".`, + defaultMessage: 'Channel name must start with a #', } ); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/use_valid_channels.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/use_valid_channels.tsx deleted file mode 100644 index 2dc6106e1e617..0000000000000 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/slack_api/use_valid_channels.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useState } from 'react'; -import { useQuery } from '@kbn/react-query'; -import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; -import type { HttpSetup } from '@kbn/core/public'; -import type { ValidChannelRouteResponse } from '../../../common/slack_api/types'; -import { INTERNAL_BASE_STACK_CONNECTORS_API_PATH } from '../../../common'; -import * as i18n from './translations'; - -interface UseValidChannelsProps { - authToken: string; - channelId: string; -} - -const validChannelIds = async ( - http: HttpSetup, - newAuthToken: string, - channelId: string -): Promise => { - return http.post( - `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_slack_api/channels/_valid`, - { - body: JSON.stringify({ - authToken: newAuthToken, - channelIds: [channelId], - }), - } - ); -}; - -export function useValidChannels(props: UseValidChannelsProps) { - const { authToken, channelId } = props; - const [channels, setChannels] = useState>([]); - const { - http, - notifications: { toasts }, - } = useKibana().services; - - const channelIdToValidate = channels.some((c: { id: string }) => c.id === channelId) - ? '' - : channelId; - const queryFn = () => { - return validChannelIds(http, authToken, channelIdToValidate); - }; - - const onErrorFn = () => { - toasts.addDanger(i18n.ERROR_VALID_CHANNELS); - }; - - const { data, isLoading, isFetching } = useQuery({ - queryKey: ['validChannels', authToken, channelIdToValidate], - queryFn, - onError: onErrorFn, - enabled: (authToken || '').length > 0 && (channelIdToValidate || '').length > 0, - refetchOnWindowFocus: false, - }); - - useEffect(() => { - if ((data?.invalidChannels ?? []).length > 0) { - toasts.addDanger(i18n.ERROR_INVALID_CHANNELS(data?.invalidChannels ?? [])); - } - if ((data?.validChannels ?? []).length > 0) { - setChannels((prevChannels) => { - return prevChannels.concat(data?.validChannels ?? []); - }); - } - }, [data, toasts]); - - const resetChannelsToValidate = useCallback( - (channelsToReset: Array<{ id: string; name: string }>) => { - if (channelsToReset.length === 0) { - setChannels([]); - } else { - setChannels((prevChannels) => { - if (prevChannels.length === 0) return channelsToReset; - return prevChannels.filter((c) => channelsToReset.some((cTr) => cTr.id === c.id)); - }); - } - }, - [] - ); - - return { - channels, - resetChannelsToValidate, - isLoading: isLoading && isFetching, - }; -} diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts index 8fbb355bd8df5..35d3607a38a01 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PluginInitializerContext, Plugin, CoreSetup, Logger } from '@kbn/core/server'; +import type { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server'; import type { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; @@ -15,11 +15,7 @@ import type { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-obje import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import { registerInferenceConnectorsUsageCollector } from './usage/inference/inference_connectors_usage_collector'; import { registerConnectorTypes } from './connector_types'; -import { - validSlackApiChannelsRoute, - getWellKnownEmailServiceRoute, - getWebhookSecretHeadersKeyRoute, -} from './routes'; +import { getWellKnownEmailServiceRoute, getWebhookSecretHeadersKeyRoute } from './routes'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import type { ConfigSchema as StackConnectorsConfigType } from './config'; @@ -39,12 +35,10 @@ export interface ConnectorsPluginsStart { export class StackConnectorsPlugin implements Plugin { - private readonly logger: Logger; private config: StackConnectorsConfigType; readonly experimentalFeatures: ExperimentalFeatures; constructor(context: PluginInitializerContext) { - this.logger = context.logger.get(); this.config = context.config.get(); this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []); } @@ -56,7 +50,6 @@ export class StackConnectorsPlugin const awsSesConfig = actions.getActionsConfigurationUtilities().getAwsSesConfig(); getWellKnownEmailServiceRoute(router, awsSesConfig); - validSlackApiChannelsRoute(router, actions.getActionsConfigurationUtilities(), this.logger); getWebhookSecretHeadersKeyRoute(router, core.getStartServices, this.experimentalFeatures); registerConnectorTypes({ diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/routes/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/routes/index.ts index a4c6d4d8c5e02..df595cb4256fe 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/routes/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/routes/index.ts @@ -6,5 +6,4 @@ */ export { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; -export { validSlackApiChannelsRoute } from './valid_slack_api_channels'; export { getWebhookSecretHeadersKeyRoute } from './get_webhook_secret_headers_key'; diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/routes/valid_slack_api_channels.ts b/x-pack/platform/plugins/shared/stack_connectors/server/routes/valid_slack_api_channels.ts deleted file mode 100644 index 0144c5d4e07c2..0000000000000 --- a/x-pack/platform/plugins/shared/stack_connectors/server/routes/valid_slack_api_channels.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { - IRouter, - RequestHandlerContext, - KibanaRequest, - IKibanaResponse, - KibanaResponseFactory, - Logger, -} from '@kbn/core/server'; -import type { AxiosResponse } from 'axios'; -import axios from 'axios'; -import { request } from '@kbn/actions-plugin/server/lib/axios_utils'; -import type { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config'; -import { SLACK_URL } from '@kbn/connector-schemas/slack_api'; -import { INTERNAL_BASE_STACK_CONNECTORS_API_PATH } from '../../common'; - -import type { ValidChannelResponse } from '../../common/slack_api/types'; - -const bodySchema = schema.object({ - authToken: schema.string(), - channelIds: schema.arrayOf(schema.string(), { minSize: 1, maxSize: 25 }), -}); - -export const validSlackApiChannelsRoute = ( - router: IRouter, - configurationUtilities: ActionsConfigurationUtilities, - logger: Logger -) => { - router.post( - { - path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_slack_api/channels/_valid`, - security: { - authz: { - enabled: false, - reason: - "This route is opted out from authorization as it relies on Slack's own authorization.", - }, - }, - validate: { - body: bodySchema, - }, - options: { - access: 'internal', - }, - }, - handler - ); - - async function handler( - ctx: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise { - const { authToken, channelIds } = req.body; - - const axiosInstance = axios.create(); - - const validChannelId = (channelId = ''): Promise> => { - return request({ - axios: axiosInstance, - configurationUtilities, - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-type': 'application/json; charset=UTF-8', - }, - logger, - method: 'get', - url: `${SLACK_URL}conversations.info?channel=${channelId}`, - }); - }; - - const promiseValidChannels = []; - for (const channelId of channelIds) { - promiseValidChannels.push(validChannelId(channelId)); - } - const validChannels: Array<{ id: string; name: string }> = []; - const invalidChannels: string[] = []; - const resultValidChannels = await Promise.all(promiseValidChannels); - - resultValidChannels.forEach((result, resultIdx) => { - if (result.data.ok && result.data?.channel) { - const { id, name } = result.data?.channel; - validChannels.push({ id, name }); - } else if (result.data.error && channelIds[resultIdx]) { - invalidChannels.push(channelIds[resultIdx]); - } - }); - - return res.ok({ - body: { - validChannels, - invalidChannels, - }, - }); - } -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx index 700293edecc65..8a4e6e70fc558 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { FormTestProvider } from './test_utils'; import type { ConfigFieldSchema, SecretsFieldSchema } from './simple_connector_form'; import { SimpleConnectorForm } from './simple_connector_form'; @@ -120,9 +120,7 @@ describe('SimpleConnectorForm', () => { await fillForm(res); - await act(async () => { - await userEvent.click(res.getByTestId('form-test-provide-submit')); - }); + await userEvent.click(res.getByTestId('form-test-provide-submit')); expect(onSubmit).toHaveBeenCalledWith({ data: { @@ -162,6 +160,7 @@ describe('SimpleConnectorForm', () => { await fillForm(res); await userEvent.clear(res.getByTestId(field)); + if (value !== '') { await userEvent.type(res.getByTestId(field), value, { delay: 10, @@ -172,5 +171,40 @@ describe('SimpleConnectorForm', () => { expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); }); + + it('supports custom validators', async () => { + const configSchemaWitCustomValidatos = [ + { + id: 'test', + label: 'Test', + type: 'TEXT' as const, + // so only the custom validator will run + isRequired: false, + validations: [ + { + validator: () => { + return { message: 'Invalid' }; + }, + }, + ], + }, + ]; + + const res = render( + + + + ); + + await userEvent.click(res.getByTestId('form-test-provide-submit')); + + expect(await screen.findByText('Invalid')); + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx index 80133e9b019dd..89c228021fbf3 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/components/simple_connector_form.tsx @@ -13,23 +13,27 @@ import { Field, PasswordField, } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import type { ValidationConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { FIELD_TYPES, getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { i18n } from '@kbn/i18n'; -export interface CommonFieldSchema { +type Validations = Array>; + +export interface CommonFieldSchema { id: string; label: string; helpText?: string | ReactNode; isRequired?: boolean; type?: keyof typeof FIELD_TYPES; euiFieldProps?: Record; + validations?: Validations; } -export interface ConfigFieldSchema extends CommonFieldSchema { +export interface ConfigFieldSchema extends CommonFieldSchema { isUrlField?: boolean; requireTld?: boolean; - defaultValue?: string | string[]; + defaultValue?: T; } export interface SecretsFieldSchema extends CommonFieldSchema { @@ -41,7 +45,6 @@ interface SimpleConnectorFormProps { readOnly: boolean; configFormSchema: ConfigFieldSchema[]; secretsFormSchema: SecretsFieldSchema[]; - configFormSchemaAfterSecrets?: ConfigFieldSchema[]; } type FormRowProps = ConfigFieldSchema & SecretsFieldSchema & { readOnly: boolean }; @@ -50,20 +53,22 @@ const UseTextField = getUseField({ component: Field }); const UseComboBoxField = getUseField({ component: ComboBoxField }); const { emptyField, urlField } = fieldValidators; -const getFieldConfig = ({ +const getFieldConfig = ({ label, isRequired = true, isUrlField = false, requireTld = true, defaultValue, type, + validations = [], }: { label: string; isRequired?: boolean; isUrlField?: boolean; requireTld?: boolean; - defaultValue?: string | string[]; + defaultValue?: T; type?: keyof typeof FIELD_TYPES; + validations?: Validations; }) => ({ label, validations: [ @@ -97,6 +102,7 @@ const getFieldConfig = ({ }, ] : []), + ...validations, ], defaultValue, ...(type && FIELD_TYPES[type] @@ -124,6 +130,7 @@ const FormRow: React.FC = ({ euiFieldProps = {}, type, requireTld, + validations = [], }) => { const dataTestSub = `${id}-input`; const UseField = getComponentByType(type); @@ -141,6 +148,7 @@ const FormRow: React.FC = ({ type, isRequired, requireTld, + validations, })} helpText={helpText} componentProps={{ @@ -155,7 +163,7 @@ const FormRow: React.FC = ({ ) : ( = ({ readOnly, configFormSchema, secretsFormSchema, - configFormSchemaAfterSecrets = [], }) => { return ( <> @@ -214,12 +221,6 @@ const SimpleConnectorFormComponent: React.FC = ({ {index !== secretsFormSchema.length ? : null} ))} - {configFormSchemaAfterSecrets.map(({ id, ...restConfigSchemaAfterSecrets }, index) => ( - - - {index !== configFormSchemaAfterSecrets.length ? : null} - - ))} ); }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx index 88006ce00c80c..e6cca7f265f8c 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx @@ -98,6 +98,13 @@ const EditConnectorFlyoutComponent: React.FC = ({ const [testExecutionActionParams, setTestExecutionActionParams] = useState< Record >({}); + + const onEditAction = useCallback( + (field: string, value: unknown) => + setTestExecutionActionParams((oldParams) => ({ ...oldParams, [field]: value })), + [] + ); + const [testExecutionResult, setTestExecutionResult] = useState | undefined>>(none); @@ -294,7 +301,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ connector={connector} executeEnabled={!isFormModified} actionParams={testExecutionActionParams} - setActionParams={setTestExecutionActionParams} + onEditAction={onEditAction} onExecutionAction={onExecutionAction} isExecutingAction={isExecutingConnector} executionResult={testExecutionResult} @@ -303,12 +310,13 @@ const EditConnectorFlyoutComponent: React.FC = ({ ); }, [ connector, - actionTypeRegistry, - isExecutingConnector, isFormModified, testExecutionActionParams, - testExecutionResult, + onEditAction, onExecutionAction, + isExecutingConnector, + testExecutionResult, + actionTypeRegistry, ]); const renderConnectorRulesList = useCallback(() => { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx index ea580616225c2..1f338751e4ac4 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx @@ -104,7 +104,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={none} @@ -148,7 +148,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={none} @@ -176,7 +176,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={some({ @@ -203,7 +203,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={some({ @@ -231,7 +231,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={some({ @@ -269,7 +269,7 @@ describe('test_connector_form', () => { connector={connector} executeEnabled={true} actionParams={{}} - setActionParams={() => {}} + onEditAction={() => {}} isExecutingAction={false} onExecutionAction={async () => {}} executionResult={some(undefined)} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx index 75f2b3ff70d3b..3af13a26d2fdd 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -33,7 +33,7 @@ export interface TestConnectorFormProps { connector: ActionConnector; executeEnabled: boolean; isExecutingAction: boolean; - setActionParams: (params: Record) => void; + onEditAction: (field: string, value: unknown) => void; actionParams: Record; onExecutionAction: () => Promise; executionResult: Option | undefined>; @@ -45,7 +45,7 @@ export const TestConnectorForm = ({ executeEnabled, executionResult, actionParams, - setActionParams, + onEditAction, onExecutionAction, isExecutingAction, actionTypeRegistry, @@ -91,12 +91,7 @@ export const TestConnectorForm = ({ actionParams={actionParams} index={0} errors={actionErrors} - editAction={(field, value) => - setActionParams({ - ...actionParams, - [field]: value, - }) - } + editAction={onEditAction} messageVariables={[]} actionConnector={connector} executionMode={ActionConnectorMode.Test}