mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Actions] Microsoft Teams connector (#83169)
* First cut at adding teams connector * Getting teams connector working * Unit tests * Updating docs * PR comments * PR comments * Changing error to debug log * Fixing imports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
00e59512fa
commit
8ca1e93763
18 changed files with 1208 additions and 1 deletions
|
@ -23,6 +23,9 @@ a| <<jira-action-type, Jira>>
|
|||
|
||||
| Create an incident in Jira.
|
||||
|
||||
a| <<teams-action-type, Microsoft Teams>>
|
||||
|
||||
| Send a message to a Microsoft Teams channel.
|
||||
|
||||
a| <<pagerduty-action-type, PagerDuty>>
|
||||
|
||||
|
@ -65,6 +68,7 @@ include::action-types/email.asciidoc[]
|
|||
include::action-types/resilient.asciidoc[]
|
||||
include::action-types/index.asciidoc[]
|
||||
include::action-types/jira.asciidoc[]
|
||||
include::action-types/teams.asciidoc[]
|
||||
include::action-types/pagerduty.asciidoc[]
|
||||
include::action-types/server-log.asciidoc[]
|
||||
include::action-types/servicenow.asciidoc[]
|
||||
|
|
58
docs/user/alerting/action-types/teams.asciidoc
Normal file
58
docs/user/alerting/action-types/teams.asciidoc
Normal file
|
@ -0,0 +1,58 @@
|
|||
[role="xpack"]
|
||||
[[teams-action-type]]
|
||||
=== Microsoft Teams action
|
||||
|
||||
The Microsoft Teams action type uses https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Incoming Webhooks].
|
||||
|
||||
[float]
|
||||
[[teams-connector-configuration]]
|
||||
==== Connector configuration
|
||||
|
||||
Microsoft Teams connectors have the following configuration properties:
|
||||
|
||||
Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action.
|
||||
Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel[Add Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts.
|
||||
|
||||
[float]
|
||||
[[Preconfigured-teams-configuration]]
|
||||
==== Preconfigured action type
|
||||
|
||||
[source,text]
|
||||
--
|
||||
my-teams:
|
||||
name: preconfigured-teams-action-type
|
||||
actionTypeId: .teams
|
||||
config:
|
||||
webhookUrl: 'https://outlook.office.com/webhook/abcd@0123456/IncomingWebhook/abcdefgh/ijklmnopqrstuvwxyz'
|
||||
--
|
||||
|
||||
`config` defines the action type specific to the configuration.
|
||||
`config` contains
|
||||
`webhookUrl`, a string that corresponds to *Webhook URL*.
|
||||
|
||||
|
||||
[float]
|
||||
[[teams-action-configuration]]
|
||||
==== Action configuration
|
||||
|
||||
Microsoft Teams actions have the following properties:
|
||||
|
||||
Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported.
|
||||
|
||||
[[configuring-teams]]
|
||||
==== Configuring Microsoft Teams Accounts
|
||||
|
||||
You need a https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Microsoft Teams webhook URL] to
|
||||
configure a Microsoft Teams action. To create a webhook
|
||||
URL, add the **Incoming Webhook App** through the Microsoft Teams console:
|
||||
|
||||
. Log in to http://teams.microsoft.com[teams.microsoft.com] as a team administrator.
|
||||
. Navigate to the Apps directory, search for and select the *Incoming Webhook* app.
|
||||
. Choose _Add to team_ and select a team and channel for the app.
|
||||
. Enter a name for your webhook and (optionally) upload a custom icon.
|
||||
+
|
||||
image::images/teams-add-webhook-integration.png[]
|
||||
. Click *Create*.
|
||||
. Copy the generated webhook URL so you can paste it into your Teams connector form.
|
||||
+
|
||||
image::images/teams-copy-webhook-url.png[]
|
BIN
docs/user/alerting/images/teams-add-webhook-integration.png
Normal file
BIN
docs/user/alerting/images/teams-add-webhook-integration.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 238 KiB |
BIN
docs/user/alerting/images/teams-copy-webhook-url.png
Normal file
BIN
docs/user/alerting/images/teams-copy-webhook-url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -14,7 +14,15 @@ import { actionsConfigMock } from '../actions_config.mock';
|
|||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { licensingMock } from '../../../licensing/server/mocks';
|
||||
|
||||
const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook'];
|
||||
const ACTION_TYPE_IDS = [
|
||||
'.index',
|
||||
'.email',
|
||||
'.pagerduty',
|
||||
'.server-log',
|
||||
'.slack',
|
||||
'.teams',
|
||||
'.webhook',
|
||||
];
|
||||
|
||||
export function createActionTypeRegistry(): {
|
||||
logger: jest.Mocked<Logger>;
|
||||
|
|
|
@ -17,6 +17,7 @@ import { getActionType as getWebhookActionType } from './webhook';
|
|||
import { getActionType as getServiceNowActionType } from './servicenow';
|
||||
import { getActionType as getJiraActionType } from './jira';
|
||||
import { getActionType as getResilientActionType } from './resilient';
|
||||
import { getActionType as getTeamsActionType } from './teams';
|
||||
|
||||
export function registerBuiltInActionTypes({
|
||||
actionsConfigUtils: configurationUtilities,
|
||||
|
@ -36,4 +37,5 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities }));
|
||||
actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities }));
|
||||
}
|
||||
|
|
266
x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
Normal file
266
x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
Normal file
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { Services } from '../types';
|
||||
import { validateParams, validateSecrets } from '../lib';
|
||||
import axios from 'axios';
|
||||
import { ActionParamsType, ActionTypeSecretsType, getActionType, TeamsActionType } from './teams';
|
||||
import { actionsConfigMock } from '../actions_config.mock';
|
||||
import { actionsMock } from '../mocks';
|
||||
import { createActionTypeRegistry } from './index.test';
|
||||
import * as utils from './lib/axios_utils';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('./lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('./lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = utils.request as jest.Mock;
|
||||
|
||||
const ACTION_TYPE_ID = '.teams';
|
||||
|
||||
const services: Services = actionsMock.createServices();
|
||||
|
||||
let actionType: TeamsActionType;
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
||||
beforeAll(() => {
|
||||
const { logger, actionTypeRegistry } = createActionTypeRegistry();
|
||||
actionType = actionTypeRegistry.get<{}, ActionTypeSecretsType, ActionParamsType>(ACTION_TYPE_ID);
|
||||
mockedLogger = logger;
|
||||
});
|
||||
|
||||
describe('action registration', () => {
|
||||
test('returns action type', () => {
|
||||
expect(actionType.id).toEqual(ACTION_TYPE_ID);
|
||||
expect(actionType.name).toEqual('Microsoft Teams');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateParams()', () => {
|
||||
test('should validate and pass when params is valid', () => {
|
||||
expect(validateParams(actionType, { message: 'a message' })).toEqual({
|
||||
message: 'a message',
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and throw error when params is invalid', () => {
|
||||
expect(() => {
|
||||
validateParams(actionType, {});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action params: [message]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateParams(actionType, { message: 1 });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action params: [message]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateActionTypeSecrets()', () => {
|
||||
test('should validate and pass when config is valid', () => {
|
||||
validateSecrets(actionType, {
|
||||
webhookUrl: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate and throw error when config is invalid', () => {
|
||||
expect(() => {
|
||||
validateSecrets(actionType, {});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(actionType, { webhookUrl: 1 });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(actionType, { webhookUrl: 'fee-fi-fo-fum' });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring teams action: unable to parse host name from webhookUrl"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should validate and pass when the teams webhookUrl is added to allowedHosts', () => {
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: (url) => {
|
||||
expect(url).toEqual('https://outlook.office.com/');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' })).toEqual({
|
||||
webhookUrl: 'https://outlook.office.com/',
|
||||
});
|
||||
});
|
||||
|
||||
test('config validation returns an error if the specified URL isnt added to allowedHosts', () => {
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: {
|
||||
...actionsConfigMock.create(),
|
||||
ensureHostnameAllowed: () => {
|
||||
throw new Error(`target hostname is not added to allowedHosts`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
validateSecrets(actionType, { webhookUrl: 'https://outlook.office.com/' });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"error validating action type secrets: error configuring teams action: target hostname is not added to allowedHosts"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
beforeAll(() => {
|
||||
requestMock.mockReset();
|
||||
actionType = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
requestMock.mockReset();
|
||||
requestMock.mockResolvedValue({
|
||||
status: 200,
|
||||
statusText: '',
|
||||
data: '',
|
||||
headers: [],
|
||||
config: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the mock executor with success', async () => {
|
||||
const response = await actionType.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from teams action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"proxySettings": undefined,
|
||||
"url": "http://example.com",
|
||||
}
|
||||
`);
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actionId": "some-id",
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
"status": "ok",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('calls the mock executor with success proxy', async () => {
|
||||
const response = await actionType.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
proxySettings: {
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
},
|
||||
});
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from teams action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"proxySettings": Object {
|
||||
"proxyRejectUnauthorizedCertificates": false,
|
||||
"proxyUrl": "https://someproxyhost",
|
||||
},
|
||||
"url": "http://example.com",
|
||||
}
|
||||
`);
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actionId": "some-id",
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
"status": "ok",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
229
x-pack/plugins/actions/server/builtin_action_types/teams.ts
Normal file
229
x-pack/plugins/actions/server/builtin_action_types/teams.ts
Normal file
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import { curry, isString } from 'lodash';
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { Logger } from '../../../../../src/core/server';
|
||||
import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header';
|
||||
import { isOk, promiseResult, Result } from './lib/result_type';
|
||||
import { request } from './lib/axios_utils';
|
||||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
|
||||
export type TeamsActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>;
|
||||
export type TeamsActionTypeExecutorOptions = ActionTypeExecutorOptions<
|
||||
{},
|
||||
ActionTypeSecretsType,
|
||||
ActionParamsType
|
||||
>;
|
||||
|
||||
// secrets definition
|
||||
|
||||
export type ActionTypeSecretsType = TypeOf<typeof SecretsSchema>;
|
||||
|
||||
const secretsSchemaProps = {
|
||||
webhookUrl: schema.string(),
|
||||
};
|
||||
const SecretsSchema = schema.object(secretsSchemaProps);
|
||||
|
||||
// params definition
|
||||
|
||||
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
|
||||
|
||||
const ParamsSchema = schema.object({
|
||||
message: schema.string({ minLength: 1 }),
|
||||
});
|
||||
|
||||
// action type definition
|
||||
export function getActionType({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
}: {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}): TeamsActionType {
|
||||
return {
|
||||
id: '.teams',
|
||||
minimumLicenseRequired: 'gold',
|
||||
name: i18n.translate('xpack.actions.builtin.teamsTitle', {
|
||||
defaultMessage: 'Microsoft Teams',
|
||||
}),
|
||||
validate: {
|
||||
secrets: schema.object(secretsSchemaProps, {
|
||||
validate: curry(validateActionTypeConfig)(configurationUtilities),
|
||||
}),
|
||||
params: ParamsSchema,
|
||||
},
|
||||
executor: curry(teamsExecutor)({ logger }),
|
||||
};
|
||||
}
|
||||
|
||||
function validateActionTypeConfig(
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
secretsObject: ActionTypeSecretsType
|
||||
) {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(secretsObject.webhookUrl);
|
||||
} catch (err) {
|
||||
return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationErrorNoHostname', {
|
||||
defaultMessage: 'error configuring teams action: unable to parse host name from webhookUrl',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureHostnameAllowed(url.hostname);
|
||||
} catch (allowListError) {
|
||||
return i18n.translate('xpack.actions.builtin.teams.teamsConfigurationError', {
|
||||
defaultMessage: 'error configuring teams action: {message}',
|
||||
values: {
|
||||
message: allowListError.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// action executor
|
||||
|
||||
async function teamsExecutor(
|
||||
{ logger }: { logger: Logger },
|
||||
execOptions: TeamsActionTypeExecutorOptions
|
||||
): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
const actionId = execOptions.actionId;
|
||||
const secrets = execOptions.secrets;
|
||||
const params = execOptions.params;
|
||||
const { webhookUrl } = secrets;
|
||||
const { message } = params;
|
||||
const data = { text: message };
|
||||
|
||||
const axiosInstance = axios.create();
|
||||
|
||||
const result: Result<AxiosResponse, AxiosError> = await promiseResult(
|
||||
request({
|
||||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: webhookUrl,
|
||||
logger,
|
||||
data,
|
||||
proxySettings: execOptions.proxySettings,
|
||||
})
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
const {
|
||||
value: { status, statusText, data: responseData, headers: responseHeaders },
|
||||
} = result;
|
||||
|
||||
// Microsoft Teams connectors do not throw 429s. Rather they will return a 200 response
|
||||
// with a 429 message in the response body when the rate limit is hit
|
||||
// https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#rate-limiting-for-connectors
|
||||
if (isString(responseData) && responseData.includes('ErrorCode:ApplicationThrottled')) {
|
||||
return pipe(
|
||||
getRetryAfterIntervalFromHeaders(responseHeaders),
|
||||
map((retry) => retryResultSeconds(actionId, message, retry)),
|
||||
getOrElse(() => retryResult(actionId, message))
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`response from teams action "${actionId}": [HTTP ${status}] ${statusText}`);
|
||||
|
||||
return successResult(actionId, data);
|
||||
} else {
|
||||
const { error } = result;
|
||||
|
||||
if (error.response) {
|
||||
const { status, statusText } = error.response;
|
||||
const serviceMessage = `[${status}] ${statusText}`;
|
||||
logger.error(`error on ${actionId} Microsoft Teams event: ${serviceMessage}`);
|
||||
|
||||
// special handling for 5xx
|
||||
if (status >= 500) {
|
||||
return retryResult(actionId, serviceMessage);
|
||||
}
|
||||
|
||||
return errorResultInvalid(actionId, serviceMessage);
|
||||
}
|
||||
|
||||
logger.debug(`error on ${actionId} Microsoft Teams action: unexpected error`);
|
||||
return errorResultUnexpectedError(actionId);
|
||||
}
|
||||
}
|
||||
|
||||
function successResult(actionId: string, data: unknown): ActionTypeExecutorResult<unknown> {
|
||||
return { status: 'ok', data, actionId };
|
||||
}
|
||||
|
||||
function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate('xpack.actions.builtin.teams.unreachableErrorMessage', {
|
||||
defaultMessage: 'error posting to Microsoft Teams, unexpected error',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
|
||||
function errorResultInvalid(
|
||||
actionId: string,
|
||||
serviceMessage: string
|
||||
): ActionTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate('xpack.actions.builtin.teams.invalidResponseErrorMessage', {
|
||||
defaultMessage: 'error posting to Microsoft Teams, invalid response',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
actionId,
|
||||
serviceMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function retryResult(actionId: string, message: string): ActionTypeExecutorResult<void> {
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.actions.builtin.teams.errorPostingRetryLaterErrorMessage',
|
||||
{
|
||||
defaultMessage: 'error posting a Microsoft Teams message, retry later',
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
retry: true,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
|
||||
function retryResultSeconds(
|
||||
actionId: string,
|
||||
message: string,
|
||||
retryAfter: number
|
||||
): ActionTypeExecutorResult<void> {
|
||||
const retryEpoch = Date.now() + retryAfter * 1000;
|
||||
const retry = new Date(retryEpoch);
|
||||
const retryString = retry.toISOString();
|
||||
const errMessage = i18n.translate(
|
||||
'xpack.actions.builtin.teams.errorPostingRetryDateErrorMessage',
|
||||
{
|
||||
defaultMessage: 'error posting a Microsoft Teams message, retry at {retryString}',
|
||||
values: {
|
||||
retryString,
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error',
|
||||
message: errMessage,
|
||||
retry,
|
||||
actionId,
|
||||
serviceMessage: message,
|
||||
};
|
||||
}
|
|
@ -15,6 +15,7 @@ import { ActionTypeModel } from '../../../types';
|
|||
import { getServiceNowActionType } from './servicenow';
|
||||
import { getJiraActionType } from './jira';
|
||||
import { getResilientActionType } from './resilient';
|
||||
import { getTeamsActionType } from './teams';
|
||||
|
||||
export function registerBuiltInActionTypes({
|
||||
actionTypeRegistry,
|
||||
|
@ -30,4 +31,5 @@ export function registerBuiltInActionTypes({
|
|||
actionTypeRegistry.register(getServiceNowActionType());
|
||||
actionTypeRegistry.register(getJiraActionType());
|
||||
actionTypeRegistry.register(getResilientActionType());
|
||||
actionTypeRegistry.register(getTeamsActionType());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getActionType as getTeamsActionType } from './teams';
|
|
@ -0,0 +1,131 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> <image id="image0" width="256" height="256" x="0" y="0"
|
||||
xlink:href="data:image/png;base64,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" />
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { TypeRegistry } from '../../../type_registry';
|
||||
import { registerBuiltInActionTypes } from '.././index';
|
||||
import { ActionTypeModel } from '../../../../types';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
|
||||
const ACTION_TYPE_ID = '.teams';
|
||||
let actionTypeModel: ActionTypeModel;
|
||||
|
||||
beforeAll(async () => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
registerBuiltInActionTypes({ actionTypeRegistry });
|
||||
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
|
||||
if (getResult !== null) {
|
||||
actionTypeModel = getResult;
|
||||
}
|
||||
});
|
||||
|
||||
describe('actionTypeRegistry.get() works', () => {
|
||||
test('action type static data is as expected', () => {
|
||||
expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('teams connector validation', () => {
|
||||
test('connector validation succeeds when connector config is valid', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
webhookUrl: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - empty webhook url', () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'h',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL is invalid.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('connector validation fails when connector config is not valid - invalid webhook url protocol', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http://insecure',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'team',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
|
||||
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
errors: {
|
||||
webhookUrl: ['Webhook URL must start with https://.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('teams action params validation', () => {
|
||||
test('if action params validation succeeds when action params is valid', () => {
|
||||
const actionParams = {
|
||||
message: 'message {test}',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { message: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when message is not valid', () => {
|
||||
const actionParams = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
expect(actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
message: ['Message is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import teamsSvg from './teams.svg';
|
||||
import { ActionTypeModel, ValidationResult } from '../../../../types';
|
||||
import { TeamsActionParams, TeamsSecrets, TeamsActionConnector } from '../types';
|
||||
import { isValidUrl } from '../../../lib/value_validators';
|
||||
|
||||
export function getActionType(): ActionTypeModel<unknown, TeamsSecrets, TeamsActionParams> {
|
||||
return {
|
||||
id: '.teams',
|
||||
iconClass: teamsSvg,
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Send a message to a Microsoft Teams channel.',
|
||||
}
|
||||
),
|
||||
actionTypeTitle: i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Send a message to a Microsoft Teams channel.',
|
||||
}
|
||||
),
|
||||
validateConnector: (action: TeamsActionConnector): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
webhookUrl: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!action.secrets.webhookUrl) {
|
||||
errors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
} else if (action.secrets.webhookUrl) {
|
||||
if (!isValidUrl(action.secrets.webhookUrl)) {
|
||||
errors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL is invalid.',
|
||||
}
|
||||
)
|
||||
);
|
||||
} else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) {
|
||||
errors.webhookUrl.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText',
|
||||
{
|
||||
defaultMessage: 'Webhook URL must start with https://.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (actionParams: TeamsActionParams): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./teams_connectors')),
|
||||
actionParamsFields: lazy(() => import('./teams_params')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
||||
import { act } from '@testing-library/react';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
import TeamsActionFields from './teams_connectors';
|
||||
import { DocLinksStart } from 'kibana/public';
|
||||
|
||||
describe('TeamsActionFields renders', () => {
|
||||
test('all connector fields are rendered', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'https:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
const deps = {
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
docLinks={deps!.docLinks}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').first().prop('value')).toBe(
|
||||
'https:\\test'
|
||||
);
|
||||
});
|
||||
|
||||
test('should display a message on create to remember credentials', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.teams',
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as TeamsActionConnector;
|
||||
const deps = {
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
docLinks={deps!.docLinks}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should display a message on edit to re-enter credentials', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
webhookUrl: 'http:\\test',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams',
|
||||
config: {},
|
||||
} as TeamsActionConnector;
|
||||
const deps = {
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart,
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsActionFields
|
||||
action={actionConnector}
|
||||
errors={{ index: [], webhookUrl: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
docLinks={deps!.docLinks}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ActionConnectorFieldsProps } from '../../../../types';
|
||||
import { TeamsActionConnector } from '../types';
|
||||
|
||||
const TeamsActionFields: React.FunctionComponent<ActionConnectorFieldsProps<
|
||||
TeamsActionConnector
|
||||
>> = ({ action, editActionSecrets, errors, readOnly, docLinks }) => {
|
||||
const { webhookUrl } = action.secrets;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
id="webhookUrl"
|
||||
fullWidth
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel"
|
||||
defaultMessage="Create a Microsoft Teams Webhook URL"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
error={errors.webhookUrl}
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Webhook URL',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Fragment>
|
||||
{getEncryptedFieldNotifyLabel(!action.id)}
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={errors.webhookUrl.length > 0 && webhookUrl !== undefined}
|
||||
name="webhookUrl"
|
||||
readOnly={readOnly}
|
||||
value={webhookUrl || ''}
|
||||
data-test-subj="teamsWebhookUrlInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('webhookUrl', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!webhookUrl) {
|
||||
editActionSecrets('webhookUrl', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
function getEncryptedFieldNotifyLabel(isCreate: boolean) {
|
||||
if (isCreate) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" data-test-subj="rememberValuesMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.rememberValueLabel"
|
||||
defaultMessage="Remember this value. You must reenter it each time you edit the connector."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="reenterValuesMessage"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel',
|
||||
{ defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' }
|
||||
)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TeamsActionFields as default };
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import TeamsParamsFields from './teams_params';
|
||||
import { DocLinksStart } from 'kibana/public';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
|
||||
describe('TeamsParamsFields renders', () => {
|
||||
test('all params fields is rendered', () => {
|
||||
const mocks = coreMock.createSetup();
|
||||
const actionParams = {
|
||||
message: 'test message',
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<TeamsParamsFields
|
||||
actionParams={actionParams}
|
||||
errors={{ message: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
|
||||
toastNotifications={mocks.notifications.toasts}
|
||||
http={mocks.http}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="messageTextArea"]').first().prop('value')).toStrictEqual(
|
||||
'test message'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import { TeamsActionParams } from '../types';
|
||||
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
|
||||
|
||||
const TeamsParamsFields: React.FunctionComponent<ActionParamsProps<TeamsActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
defaultMessage,
|
||||
}) => {
|
||||
const { message } = actionParams;
|
||||
useEffect(() => {
|
||||
if (!message && defaultMessage && defaultMessage.length > 0) {
|
||||
editAction('message', defaultMessage, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editAction}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'message'}
|
||||
inputTargetValue={message}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
)}
|
||||
errors={errors.message as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TeamsParamsFields as default };
|
|
@ -60,6 +60,10 @@ export interface SlackActionParams {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface TeamsActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WebhookActionParams {
|
||||
body?: string;
|
||||
}
|
||||
|
@ -119,3 +123,9 @@ export interface WebhookSecrets {
|
|||
}
|
||||
|
||||
export type WebhookActionConnector = UserConfiguredActionConnector<WebhookConfig, WebhookSecrets>;
|
||||
|
||||
export interface TeamsSecrets {
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export type TeamsActionConnector = UserConfiguredActionConnector<unknown, TeamsSecrets>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue